Initial commit
This commit is contained in:
commit
9b6723e11e
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
10
.idea/Info_edupage.iml
Normal file
10
.idea/Info_edupage.iml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
7
.idea/discord.xml
Normal file
7
.idea/discord.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N803" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (Info_edupage)" project-jdk-type="Python SDK" />
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Info_edupage.iml" filepath="$PROJECT_DIR$/.idea/Info_edupage.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
241
.venv/bin/Activate.ps1
Normal file
241
.venv/bin/Activate.ps1
Normal file
@ -0,0 +1,241 @@
|
||||
<#
|
||||
.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 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 virutal 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
|
||||
}
|
||||
}
|
||||
|
||||
# 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"
|
66
.venv/bin/activate
Normal file
66
.venv/bin/activate
Normal file
@ -0,0 +1,66 @@
|
||||
# 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
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV="/home/untriex/Documents/Info_edupage/.venv"
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_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
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
25
.venv/bin/activate.csh
Normal file
25
.venv/bin/activate.csh
Normal file
@ -0,0 +1,25 @@
|
||||
# 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; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/home/untriex/Documents/Info_edupage/.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"
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
64
.venv/bin/activate.fish
Normal file
64
.venv/bin/activate.fish
Normal file
@ -0,0 +1,64 @@
|
||||
# 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"
|
||||
functions -e fish_prompt
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/home/untriex/Documents/Info_edupage/.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"
|
||||
end
|
8
.venv/bin/chardetect
Executable file
8
.venv/bin/chardetect
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from chardet.cli.chardetect import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
.venv/bin/f2py
Executable file
8
.venv/bin/f2py
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from numpy.f2py.f2py2e import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
.venv/bin/f2py3
Executable file
8
.venv/bin/f2py3
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from numpy.f2py.f2py2e import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
.venv/bin/f2py3.9
Executable file
8
.venv/bin/f2py3.9
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from numpy.f2py.f2py2e import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
8
.venv/bin/pip
Executable file
8
.venv/bin/pip
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- 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())
|
8
.venv/bin/pip3
Executable file
8
.venv/bin/pip3
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- 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())
|
8
.venv/bin/pip3.9
Executable file
8
.venv/bin/pip3.9
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- 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())
|
1
.venv/bin/python
Symbolic link
1
.venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3.9
|
1
.venv/bin/python3
Symbolic link
1
.venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
python3.9
|
1
.venv/bin/python3.9
Symbolic link
1
.venv/bin/python3.9
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/bin/python3.9
|
8
.venv/bin/wheel
Executable file
8
.venv/bin/wheel
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/untriex/Documents/Info_edupage/.venv/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from wheel.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
@ -0,0 +1 @@
|
||||
pip
|
@ -0,0 +1,77 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MouseInfo
|
||||
Version: 0.1.3
|
||||
Summary: An application to display XY position and RGB color information for the pixel currently under the mouse. Works on Python 2 and 3.
|
||||
Home-page: https://github.com/asweigart/mouseinfo
|
||||
Author: Al Sweigart
|
||||
Author-email: al@inventwithpython.com
|
||||
License: GPLv3+
|
||||
Platform: UNKNOWN
|
||||
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.2
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: pyperclip
|
||||
Requires-Dist: rubicon-objc ; platform_system == "Darwin"
|
||||
Requires-Dist: Xlib ; platform_system == "Linux" and python_version < "3.0"
|
||||
Requires-Dist: python3-Xlib ; platform_system == "Linux" and python_version >= "3.0"
|
||||
Requires-Dist: Pillow (>=2.0.0) ; python_version == "2.7"
|
||||
Requires-Dist: Pillow (<=3.4.2,>=2.0.0) ; python_version == "3.2"
|
||||
Requires-Dist: Pillow (<=4.3.0,>=2.0.0) ; python_version == "3.3"
|
||||
Requires-Dist: Pillow (<=5.4.1,>=2.5.0) ; python_version == "3.4"
|
||||
Requires-Dist: Pillow (>=3.2.0) ; python_version == "3.5"
|
||||
Requires-Dist: Pillow (>=4.0.0) ; python_version == "3.6"
|
||||
Requires-Dist: Pillow (>=5.2.0) ; python_version == "3.7"
|
||||
|
||||
MouseInfo
|
||||
======
|
||||
|
||||
An application to display XY position and RGB color information for the pixel currently under the mouse. Works on Python 2 and 3. This is useful for GUI automation planning.
|
||||
|
||||
The full documentation is at https://mouseinfo.readthedocs.io/en/latest/
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
To install with pip, run:
|
||||
|
||||
pip install mouseinfo
|
||||
|
||||
Quickstart Guide
|
||||
----------------
|
||||
|
||||
To run this application, enter the following into the terminal:
|
||||
|
||||
python3 -m mouseinfo
|
||||
|
||||
Or for Python 2, run:
|
||||
|
||||
python -m mouseinfo
|
||||
|
||||
Alternatively, to run it from the interactive shell or a Python program:
|
||||
|
||||
>>> import mouseinfo
|
||||
>>> mouseinfo.mouseInfo()
|
||||
|
||||
The Mouse Info application displays the current XY coordinates of the mouse cursor, as well as the RGB color information of the pixel directly under the cursor. This can be useful for planning out GUI automation tests where the mouse is controlled by a script (such as a Python script with PyAutoGUI) to click on the screen at specific coordinates.
|
||||
|
||||
The "Copy" buttons will copy this mouse information to the clipboard, while the "Log" buttons will add this mouse information to the text field in the application. The RGB color information is given as a comman-delimited, three-integer red, green, and blue values as decimals from 0 to 255. The hex values of the RGB value is also given.
|
||||
|
||||
For practical use, you should set the keyboard focus on these buttons by tabbing over them. This leaves you free to move the mouse into position and then press space or Enter to log the current mouse coordinates/RGB value.
|
||||
|
||||
The contents of the log text field can be saved by clicking "Save Log". This will automatically overwrite any file with the provided name. A screenshot can also be saved by clicking "Save Screenshot"
|
||||
|
||||
Contribute
|
||||
----------
|
||||
|
||||
If you'd like to contribute to MouseInfo, check out https://github.com/asweigart/mouseinfo
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
MouseInfo-0.1.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MouseInfo-0.1.3.dist-info/METADATA,sha256=kYuj0ypUuidPVs8ud01-CQP-DAguMx0NS8wtjbOEF-g,3507
|
||||
MouseInfo-0.1.3.dist-info/RECORD,,
|
||||
MouseInfo-0.1.3.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
|
||||
MouseInfo-0.1.3.dist-info/top_level.txt,sha256=OzKM4RMItUSZcDQKYRg00PHK3qPsWsG-9RQxsReBf5k,10
|
||||
mouseinfo/__init__.py,sha256=LmQ_b7eBPbnxfM4pu9nFykcEgMcmLspkvQfp11Qg-P8,42960
|
||||
mouseinfo/__main__.py,sha256=1OvUG2YiESIfQGuhhq-iEWtNXBa8-VGxGBhY4stfkw0,81
|
||||
mouseinfo/__pycache__/__init__.cpython-39.pyc,,
|
||||
mouseinfo/__pycache__/__main__.cpython-39.pyc,,
|
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.37.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
@ -0,0 +1 @@
|
||||
mouseinfo
|
110
.venv/lib/python3.9/site-packages/PIL/BdfFontFile.py
Normal file
110
.venv/lib/python3.9/site-packages/PIL/BdfFontFile.py
Normal file
@ -0,0 +1,110 @@
|
||||
#
|
||||
# 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 . import FontFile, Image
|
||||
|
||||
bdf_slant = {
|
||||
"R": "Roman",
|
||||
"I": "Italic",
|
||||
"O": "Oblique",
|
||||
"RI": "Reverse Italic",
|
||||
"RO": "Reverse Oblique",
|
||||
"OT": "Other",
|
||||
}
|
||||
|
||||
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
|
||||
|
||||
|
||||
def bdf_char(f):
|
||||
# skip to STARTCHAR
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s:
|
||||
return None
|
||||
if s[:9] == b"STARTCHAR":
|
||||
break
|
||||
id = s[9:].strip().decode("ascii")
|
||||
|
||||
# load symbol properties
|
||||
props = {}
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s or s[:6] == b"BITMAP":
|
||||
break
|
||||
i = s.find(b" ")
|
||||
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||
|
||||
# load bitmap
|
||||
bitmap = []
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s or s[:7] == b"ENDCHAR":
|
||||
break
|
||||
bitmap.append(s[:-1])
|
||||
bitmap = b"".join(bitmap)
|
||||
|
||||
[x, y, l, d] = [int(p) for p in props["BBX"].split()]
|
||||
[dx, dy] = [int(p) for p in props["DWIDTH"].split()]
|
||||
|
||||
bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y)
|
||||
|
||||
try:
|
||||
im = Image.frombytes("1", (x, y), bitmap, "hex", "1")
|
||||
except ValueError:
|
||||
# deal with zero-width characters
|
||||
im = Image.new("1", (x, y))
|
||||
|
||||
return id, int(props["ENCODING"]), bbox, im
|
||||
|
||||
|
||||
class BdfFontFile(FontFile.FontFile):
|
||||
"""Font file plugin for the X11 BDF format."""
|
||||
|
||||
def __init__(self, fp):
|
||||
super().__init__()
|
||||
|
||||
s = fp.readline()
|
||||
if s[:13] != b"STARTFONT 2.1":
|
||||
raise SyntaxError("not a valid BDF file")
|
||||
|
||||
props = {}
|
||||
comments = []
|
||||
|
||||
while True:
|
||||
s = fp.readline()
|
||||
if not s or s[:13] == 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
|
428
.venv/lib/python3.9/site-packages/PIL/BlpImagePlugin.py
Normal file
428
.venv/lib/python3.9/site-packages/PIL/BlpImagePlugin.py
Normal file
@ -0,0 +1,428 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
BLP_FORMAT_JPEG = 0
|
||||
|
||||
BLP_ENCODING_UNCOMPRESSED = 1
|
||||
BLP_ENCODING_DXT = 2
|
||||
BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3
|
||||
|
||||
BLP_ALPHA_ENCODING_DXT1 = 0
|
||||
BLP_ALPHA_ENCODING_DXT3 = 1
|
||||
BLP_ALPHA_ENCODING_DXT5 = 7
|
||||
|
||||
|
||||
def unpack_565(i):
|
||||
return (((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3)
|
||||
|
||||
|
||||
def decode_dxt1(data, alpha=False):
|
||||
"""
|
||||
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 in range(blocks):
|
||||
# Decode next 8-byte block.
|
||||
idx = block * 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):
|
||||
"""
|
||||
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 in range(blocks):
|
||||
idx = block * 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):
|
||||
"""
|
||||
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 in range(blocks):
|
||||
idx = block * 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
|
||||
|
||||
|
||||
class BlpImageFile(ImageFile.ImageFile):
|
||||
"""
|
||||
Blizzard Mipmap Format
|
||||
"""
|
||||
|
||||
format = "BLP"
|
||||
format_description = "Blizzard Mipmap Format"
|
||||
|
||||
def _open(self):
|
||||
self.magic = self.fp.read(4)
|
||||
self._read_blp_header()
|
||||
|
||||
if self.magic == b"BLP1":
|
||||
decoder = "BLP1"
|
||||
self.mode = "RGB"
|
||||
elif self.magic == b"BLP2":
|
||||
decoder = "BLP2"
|
||||
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
|
||||
else:
|
||||
raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
|
||||
|
||||
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
|
||||
|
||||
def _read_blp_header(self):
|
||||
(self._blp_compression,) = struct.unpack("<i", self.fp.read(4))
|
||||
|
||||
(self._blp_encoding,) = struct.unpack("<b", self.fp.read(1))
|
||||
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
|
||||
(self._blp_alpha_encoding,) = struct.unpack("<b", self.fp.read(1))
|
||||
(self._blp_mips,) = struct.unpack("<b", self.fp.read(1))
|
||||
|
||||
self._size = struct.unpack("<II", self.fp.read(8))
|
||||
|
||||
if self.magic == b"BLP1":
|
||||
# Only present for BLP1
|
||||
(self._blp_encoding,) = struct.unpack("<i", self.fp.read(4))
|
||||
(self._blp_subtype,) = struct.unpack("<i", self.fp.read(4))
|
||||
|
||||
self._blp_offsets = struct.unpack("<16I", self.fp.read(16 * 4))
|
||||
self._blp_lengths = struct.unpack("<16I", self.fp.read(16 * 4))
|
||||
|
||||
|
||||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
try:
|
||||
self.fd.seek(0)
|
||||
self.magic = self.fd.read(4)
|
||||
self._read_blp_header()
|
||||
self._load()
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated Blp file") from e
|
||||
return 0, 0
|
||||
|
||||
def _safe_read(self, length):
|
||||
return ImageFile._safe_read(self.fd, length)
|
||||
|
||||
def _read_palette(self):
|
||||
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_blp_header(self):
|
||||
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
|
||||
|
||||
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
|
||||
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
|
||||
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
|
||||
(self._blp_mips,) = struct.unpack("<b", self._safe_read(1))
|
||||
|
||||
self.size = struct.unpack("<II", self._safe_read(8))
|
||||
|
||||
if self.magic == b"BLP1":
|
||||
# Only present for BLP1
|
||||
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
|
||||
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4))
|
||||
|
||||
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
|
||||
|
||||
class BLP1Decoder(_BLPBaseDecoder):
|
||||
def _load(self):
|
||||
if self._blp_compression == BLP_FORMAT_JPEG:
|
||||
self._decode_jpeg_stream()
|
||||
|
||||
elif self._blp_compression == 1:
|
||||
if self._blp_encoding in (4, 5):
|
||||
data = bytearray()
|
||||
palette = self._read_palette()
|
||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
||||
while True:
|
||||
try:
|
||||
(offset,) = struct.unpack("<B", _data.read(1))
|
||||
except struct.error:
|
||||
break
|
||||
b, g, r, a = palette[offset]
|
||||
data.extend([r, g, b])
|
||||
|
||||
self.set_as_raw(bytes(data))
|
||||
else:
|
||||
raise BLPFormatError(
|
||||
f"Unsupported BLP encoding {repr(self._blp_encoding)}"
|
||||
)
|
||||
else:
|
||||
raise BLPFormatError(
|
||||
f"Unsupported BLP compression {repr(self._blp_encoding)}"
|
||||
)
|
||||
|
||||
def _decode_jpeg_stream(self):
|
||||
from PIL.JpegImagePlugin import JpegImageFile
|
||||
|
||||
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
||||
jpeg_header = self._safe_read(jpeg_header_size)
|
||||
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
|
||||
data = self._safe_read(self._blp_lengths[0])
|
||||
data = jpeg_header + data
|
||||
data = BytesIO(data)
|
||||
image = JpegImageFile(data)
|
||||
Image._decompression_bomb_check(image.size)
|
||||
self.tile = image.tile # :/
|
||||
self.fd = image.fp
|
||||
self.mode = image.mode
|
||||
|
||||
|
||||
class BLP2Decoder(_BLPBaseDecoder):
|
||||
def _load(self):
|
||||
palette = self._read_palette()
|
||||
|
||||
data = bytearray()
|
||||
self.fd.seek(self._blp_offsets[0])
|
||||
|
||||
if self._blp_compression == 1:
|
||||
# Uncompressed or DirectX compression
|
||||
|
||||
if self._blp_encoding == BLP_ENCODING_UNCOMPRESSED:
|
||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
||||
while True:
|
||||
try:
|
||||
(offset,) = struct.unpack("<B", _data.read(1))
|
||||
except struct.error:
|
||||
break
|
||||
b, g, r, a = palette[offset]
|
||||
data.extend((r, g, b))
|
||||
|
||||
elif self._blp_encoding == BLP_ENCODING_DXT:
|
||||
if self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT1:
|
||||
linesize = (self.size[0] + 3) // 4 * 8
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
for d in decode_dxt1(
|
||||
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
|
||||
):
|
||||
data += d
|
||||
|
||||
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT3:
|
||||
linesize = (self.size[0] + 3) // 4 * 16
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
for d in decode_dxt3(self._safe_read(linesize)):
|
||||
data += d
|
||||
|
||||
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT5:
|
||||
linesize = (self.size[0] + 3) // 4 * 16
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
for d in decode_dxt5(self._safe_read(linesize)):
|
||||
data += d
|
||||
else:
|
||||
raise BLPFormatError(
|
||||
f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
|
||||
)
|
||||
else:
|
||||
raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}")
|
||||
|
||||
else:
|
||||
raise BLPFormatError(
|
||||
f"Unknown BLP compression {repr(self._blp_compression)}"
|
||||
)
|
||||
|
||||
self.set_as_raw(bytes(data))
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] in (b"BLP1", b"BLP2")
|
||||
|
||||
|
||||
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
|
||||
Image.register_extension(BlpImageFile.format, ".blp")
|
||||
|
||||
Image.register_decoder("BLP1", BLP1Decoder)
|
||||
Image.register_decoder("BLP2", BLP2Decoder)
|
378
.venv/lib/python3.9/site-packages/PIL/BmpImagePlugin.py
Normal file
378
.venv/lib/python3.9/site-packages/PIL/BmpImagePlugin.py
Normal file
@ -0,0 +1,378 @@
|
||||
#
|
||||
# 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 . 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"),
|
||||
}
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:2] == b"BM"
|
||||
|
||||
|
||||
def _dib_accept(prefix):
|
||||
return i32(prefix) in [12, 40, 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=0, offset=0):
|
||||
""" Read relevant info about the BMP """
|
||||
read, seek = self.fp.read, self.fp.seek
|
||||
if header:
|
||||
seek(header)
|
||||
file_info = {}
|
||||
# read bmp header size @offset 14 (this is part of the header size)
|
||||
file_info["header_size"] = i32(read(4))
|
||||
file_info["direction"] = -1
|
||||
|
||||
# -------------------- If requested, read header at a specific position
|
||||
# read the rest of the bmp header, without its size
|
||||
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
|
||||
|
||||
# -------------------------------------------------- IBM OS/2 Bitmap v1
|
||||
# ----- This format has different offsets because of width/height types
|
||||
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.RAW
|
||||
file_info["palette_padding"] = 3
|
||||
|
||||
# --------------------------------------------- Windows Bitmap v2 to v5
|
||||
# v3, OS/2 v2, v4, v5
|
||||
elif file_info["header_size"] in (40, 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
|
||||
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
|
||||
if file_info["compression"] == self.BITFIELDS:
|
||||
if len(header_data) >= 52:
|
||||
for idx, mask in enumerate(
|
||||
["r_mask", "g_mask", "b_mask", "a_mask"]
|
||||
):
|
||||
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 ["r_mask", "g_mask", "b_mask"]:
|
||||
file_info[mask] = i32(read(4))
|
||||
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:
|
||||
raise OSError(f"Unsupported BMP header type ({file_info['header_size']})")
|
||||
|
||||
# ------------------ Special case : header is reported 40, which
|
||||
# ---------------------- is shorter than real size for bpp >= 16
|
||||
self._size = file_info["width"], file_info["height"]
|
||||
|
||||
# ------- If color count was not found in the header, compute from bits
|
||||
file_info["colors"] = (
|
||||
file_info["colors"]
|
||||
if file_info.get("colors", 0)
|
||||
else (1 << file_info["bits"])
|
||||
)
|
||||
|
||||
# ---------------------- Check bit depth for unusual unsupported values
|
||||
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
|
||||
if self.mode is None:
|
||||
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
|
||||
|
||||
# ---------------- Process BMP with Bitfields compression (not palette)
|
||||
if file_info["compression"] == self.BITFIELDS:
|
||||
SUPPORTED = {
|
||||
32: [
|
||||
(0xFF0000, 0xFF00, 0xFF, 0x0),
|
||||
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
|
||||
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
|
||||
(0x0, 0x0, 0x0, 0x0),
|
||||
(0xFF000000, 0xFF0000, 0xFF00, 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, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
|
||||
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
|
||||
(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"]]
|
||||
):
|
||||
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"]]
|
||||
):
|
||||
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
|
||||
else:
|
||||
raise OSError("Unsupported BMP bitfields layout")
|
||||
else:
|
||||
raise OSError("Unsupported BMP bitfields layout")
|
||||
elif file_info["compression"] == self.RAW:
|
||||
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
||||
raw_mode, self.mode = "BGRA", "RGBA"
|
||||
else:
|
||||
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
|
||||
|
||||
# --------------- 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):
|
||||
raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})")
|
||||
else:
|
||||
padding = file_info["palette_padding"]
|
||||
palette = read(padding * file_info["colors"])
|
||||
greyscale = True
|
||||
indices = (
|
||||
(0, 255)
|
||||
if file_info["colors"] == 2
|
||||
else list(range(file_info["colors"]))
|
||||
)
|
||||
|
||||
# ----------------- Check if greyscale and ignore palette if so
|
||||
for ind, val in enumerate(indices):
|
||||
rgb = palette[ind * padding : ind * padding + 3]
|
||||
if rgb != o8(val) * 3:
|
||||
greyscale = False
|
||||
|
||||
# ------- If all colors are grey, white or black, ditch palette
|
||||
if greyscale:
|
||||
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"]
|
||||
self.tile = [
|
||||
(
|
||||
"raw",
|
||||
(0, 0, file_info["width"], file_info["height"]),
|
||||
offset or self.fp.tell(),
|
||||
(
|
||||
raw_mode,
|
||||
((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3),
|
||||
file_info["direction"],
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
def _open(self):
|
||||
""" Open file, check magic number and read header """
|
||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||
head_data = self.fp.read(14)
|
||||
# choke if the file does not have the required magic bytes
|
||||
if not _accept(head_data):
|
||||
raise SyntaxError("Not a BMP file")
|
||||
# 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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Image plugin for the DIB format (BMP alias)
|
||||
# =============================================================================
|
||||
class DibImageFile(BmpImageFile):
|
||||
|
||||
format = "DIB"
|
||||
format_description = "Windows Bitmap"
|
||||
|
||||
def _open(self):
|
||||
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, fp, filename):
|
||||
_save(im, fp, filename, False)
|
||||
|
||||
|
||||
def _save(im, fp, filename, bitmap_header=True):
|
||||
try:
|
||||
rawmode, bits, colors = SAVE[im.mode]
|
||||
except KeyError as e:
|
||||
raise OSError(f"cannot write mode {im.mode} as BMP") from e
|
||||
|
||||
info = im.encoderinfo
|
||||
|
||||
dpi = info.get("dpi", (96, 96))
|
||||
|
||||
# 1 meter == 39.3701 inches
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
|
||||
|
||||
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
||||
header = 40 # or 64 for OS/2 version 2
|
||||
image = stride * im.size[1]
|
||||
|
||||
# bitmap header
|
||||
if bitmap_header:
|
||||
offset = 14 + header + colors * 4
|
||||
file_size = offset + image
|
||||
if file_size > 2 ** 32 - 1:
|
||||
raise ValueError("File size is too large for the BMP format")
|
||||
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 im.mode == "1":
|
||||
for i in (0, 255):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "L":
|
||||
for i in range(256):
|
||||
fp.write(o8(i) * 4)
|
||||
elif im.mode == "P":
|
||||
fp.write(im.im.getpalette("RGB", "BGRX"))
|
||||
|
||||
ImageFile._save(im, fp, [("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_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")
|
73
.venv/lib/python3.9/site-packages/PIL/BufrStubImagePlugin.py
Normal file
73
.venv/lib/python3.9/site-packages/PIL/BufrStubImagePlugin.py
Normal file
@ -0,0 +1,73 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific BUFR image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
|
||||
|
||||
|
||||
class BufrStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "BUFR"
|
||||
format_description = "BUFR"
|
||||
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(4)):
|
||||
raise SyntaxError("Not a BUFR file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
return _handler
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise OSError("BUFR save handler not installed")
|
||||
_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")
|
120
.venv/lib/python3.9/site-packages/PIL/ContainerIO.py
Normal file
120
.venv/lib/python3.9/site-packages/PIL/ContainerIO.py
Normal file
@ -0,0 +1,120 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import io
|
||||
|
||||
|
||||
class ContainerIO:
|
||||
"""
|
||||
A file object that provides read access to a part of an existing
|
||||
file (for example a TAR file).
|
||||
"""
|
||||
|
||||
def __init__(self, file, offset, length):
|
||||
"""
|
||||
Create file object.
|
||||
|
||||
:param file: Existing file.
|
||||
:param offset: Start of region, in bytes.
|
||||
:param length: Size of region, in bytes.
|
||||
"""
|
||||
self.fh = file
|
||||
self.pos = 0
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
self.fh.seek(offset)
|
||||
|
||||
##
|
||||
# Always false.
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
def seek(self, offset, mode=io.SEEK_SET):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
def tell(self):
|
||||
"""
|
||||
Get current file pointer.
|
||||
|
||||
:returns: Offset from start of region, in bytes.
|
||||
"""
|
||||
return self.pos
|
||||
|
||||
def read(self, n=0):
|
||||
"""
|
||||
Read data.
|
||||
|
||||
:param n: Number of bytes to read. If omitted or zero,
|
||||
read until end of region.
|
||||
:returns: An 8-bit string.
|
||||
"""
|
||||
if n:
|
||||
n = min(n, self.length - self.pos)
|
||||
else:
|
||||
n = self.length - self.pos
|
||||
if not n: # EOF
|
||||
return b"" if "b" in self.fh.mode else ""
|
||||
self.pos = self.pos + n
|
||||
return self.fh.read(n)
|
||||
|
||||
def readline(self):
|
||||
"""
|
||||
Read a line of text.
|
||||
|
||||
:returns: An 8-bit string.
|
||||
"""
|
||||
s = b"" if "b" in self.fh.mode else ""
|
||||
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:
|
||||
break
|
||||
return s
|
||||
|
||||
def readlines(self):
|
||||
"""
|
||||
Read multiple lines of text.
|
||||
|
||||
:returns: A list of 8-bit strings.
|
||||
"""
|
||||
lines = []
|
||||
while True:
|
||||
s = self.readline()
|
||||
if not s:
|
||||
break
|
||||
lines.append(s)
|
||||
return lines
|
75
.venv/lib/python3.9/site-packages/PIL/CurImagePlugin.py
Normal file
75
.venv/lib/python3.9/site-packages/PIL/CurImagePlugin.py
Normal file
@ -0,0 +1,75 @@
|
||||
#
|
||||
# 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 . import BmpImagePlugin, Image
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"\0\0\2\0"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for Windows Cursor files.
|
||||
|
||||
|
||||
class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||
|
||||
format = "CUR"
|
||||
format_description = "Windows Cursor"
|
||||
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
# check magic
|
||||
s = self.fp.read(6)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a CUR file")
|
||||
|
||||
# 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:
|
||||
raise TypeError("No cursors were found")
|
||||
|
||||
# load as bitmap
|
||||
self._bitmap(i32(m, 12) + offset)
|
||||
|
||||
# patch up the bitmap height
|
||||
self._size = self.size[0], self.size[1] // 2
|
||||
d, e, o, a = self.tile[0]
|
||||
self.tile[0] = d, (0, 0) + self.size, o, a
|
||||
|
||||
return
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(CurImageFile.format, CurImageFile, _accept)
|
||||
|
||||
Image.register_extension(CurImageFile.format, ".cur")
|
89
.venv/lib/python3.9/site-packages/PIL/DcxImagePlugin.py
Normal file
89
.venv/lib/python3.9/site-packages/PIL/DcxImagePlugin.py
Normal file
@ -0,0 +1,89 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
from ._binary import i32le as i32
|
||||
from .PcxImagePlugin import PcxImageFile
|
||||
|
||||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
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):
|
||||
|
||||
# Header
|
||||
s = self.fp.read(4)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a DCX file")
|
||||
|
||||
# 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 = None
|
||||
self.n_frames = len(self._offset)
|
||||
self.is_animated = self.n_frames > 1
|
||||
self.seek(0)
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.frame = frame
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self._offset[frame])
|
||||
PcxImageFile._open(self)
|
||||
|
||||
def tell(self):
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
|
||||
|
||||
Image.register_extension(DcxImageFile.format, ".dcx")
|
247
.venv/lib/python3.9/site-packages/PIL/DdsImagePlugin.py
Normal file
247
.venv/lib/python3.9/site-packages/PIL/DdsImagePlugin.py
Normal file
@ -0,0 +1,247 @@
|
||||
"""
|
||||
A Pillow loader 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/
|
||||
"""
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import o32le as o32
|
||||
|
||||
# Magic ("DDS ")
|
||||
DDS_MAGIC = 0x20534444
|
||||
|
||||
# DDS flags
|
||||
DDSD_CAPS = 0x1
|
||||
DDSD_HEIGHT = 0x2
|
||||
DDSD_WIDTH = 0x4
|
||||
DDSD_PITCH = 0x8
|
||||
DDSD_PIXELFORMAT = 0x1000
|
||||
DDSD_MIPMAPCOUNT = 0x20000
|
||||
DDSD_LINEARSIZE = 0x80000
|
||||
DDSD_DEPTH = 0x800000
|
||||
|
||||
# DDS caps
|
||||
DDSCAPS_COMPLEX = 0x8
|
||||
DDSCAPS_TEXTURE = 0x1000
|
||||
DDSCAPS_MIPMAP = 0x400000
|
||||
|
||||
DDSCAPS2_CUBEMAP = 0x200
|
||||
DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
|
||||
DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
|
||||
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
|
||||
DDSCAPS2_VOLUME = 0x200000
|
||||
|
||||
# Pixel Format
|
||||
DDPF_ALPHAPIXELS = 0x1
|
||||
DDPF_ALPHA = 0x2
|
||||
DDPF_FOURCC = 0x4
|
||||
DDPF_PALETTEINDEXED8 = 0x20
|
||||
DDPF_RGB = 0x40
|
||||
DDPF_LUMINANCE = 0x20000
|
||||
|
||||
|
||||
# dds.h
|
||||
|
||||
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
|
||||
DXT1_FOURCC = 0x31545844
|
||||
|
||||
# DXT3
|
||||
DXT3_FOURCC = 0x33545844
|
||||
|
||||
# DXT5
|
||||
DXT5_FOURCC = 0x35545844
|
||||
|
||||
|
||||
# dxgiformat.h
|
||||
|
||||
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM = 28
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
|
||||
DXGI_FORMAT_BC5_TYPELESS = 82
|
||||
DXGI_FORMAT_BC5_UNORM = 83
|
||||
DXGI_FORMAT_BC5_SNORM = 84
|
||||
DXGI_FORMAT_BC7_TYPELESS = 97
|
||||
DXGI_FORMAT_BC7_UNORM = 98
|
||||
DXGI_FORMAT_BC7_UNORM_SRGB = 99
|
||||
|
||||
|
||||
class DdsImageFile(ImageFile.ImageFile):
|
||||
format = "DDS"
|
||||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self):
|
||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
||||
if header_size != 124:
|
||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||
header_bytes = self.fp.read(header_size - 4)
|
||||
if len(header_bytes) != 120:
|
||||
raise OSError(f"Incomplete header: {len(header_bytes)} bytes")
|
||||
header = BytesIO(header_bytes)
|
||||
|
||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||
self._size = (width, height)
|
||||
self.mode = "RGBA"
|
||||
|
||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||
struct.unpack("<11I", header.read(44)) # reserved
|
||||
|
||||
# pixel format
|
||||
pfsize, pfflags = struct.unpack("<2I", header.read(8))
|
||||
fourcc = header.read(4)
|
||||
(bitcount,) = struct.unpack("<I", header.read(4))
|
||||
masks = struct.unpack("<4I", header.read(16))
|
||||
if pfflags & DDPF_RGB:
|
||||
# Texture contains uncompressed RGB data
|
||||
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
|
||||
rawmode = ""
|
||||
if bitcount == 32:
|
||||
rawmode += masks[0xFF000000]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
|
||||
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
|
||||
else:
|
||||
data_start = header_size + 4
|
||||
n = 0
|
||||
if fourcc == b"DXT1":
|
||||
self.pixel_format = "DXT1"
|
||||
n = 1
|
||||
elif fourcc == b"DXT3":
|
||||
self.pixel_format = "DXT3"
|
||||
n = 2
|
||||
elif fourcc == b"DXT5":
|
||||
self.pixel_format = "DXT5"
|
||||
n = 3
|
||||
elif fourcc == b"BC5S":
|
||||
self.pixel_format = "BC5S"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
elif fourcc == b"DX10":
|
||||
data_start += 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_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
|
||||
self.pixel_format = "BC5"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
|
||||
self.pixel_format = "BC5S"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
|
||||
self.pixel_format = "BC7"
|
||||
n = 7
|
||||
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
|
||||
self.pixel_format = "BC7"
|
||||
self.info["gamma"] = 1 / 2.2
|
||||
n = 7
|
||||
elif dxgi_format in (
|
||||
DXGI_FORMAT_R8G8B8A8_TYPELESS,
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM,
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
|
||||
):
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, ("RGBA", 0, 1))]
|
||||
if dxgi_format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
||||
self.info["gamma"] = 1 / 2.2
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unimplemented DXGI format {dxgi_format}"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}")
|
||||
|
||||
self.tile = [
|
||||
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format))
|
||||
]
|
||||
|
||||
def load_seek(self, pos):
|
||||
pass
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if im.mode not in ("RGB", "RGBA"):
|
||||
raise OSError(f"cannot write mode {im.mode} as DDS")
|
||||
|
||||
fp.write(
|
||||
o32(DDS_MAGIC)
|
||||
+ o32(124) # header size
|
||||
+ o32(
|
||||
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
|
||||
) # flags
|
||||
+ o32(im.height)
|
||||
+ o32(im.width)
|
||||
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch
|
||||
+ o32(0) # depth
|
||||
+ o32(0) # mipmaps
|
||||
+ o32(0) * 11 # reserved
|
||||
+ o32(32) # pfsize
|
||||
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags
|
||||
+ o32(0) # fourcc
|
||||
+ o32(32 if im.mode == "RGBA" else 24) # bitcount
|
||||
+ o32(0xFF0000) # rbitmask
|
||||
+ o32(0xFF00) # gbitmask
|
||||
+ o32(0xFF) # bbitmask
|
||||
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
|
||||
+ o32(DDSCAPS_TEXTURE) # dwCaps
|
||||
+ o32(0) # dwCaps2
|
||||
+ o32(0) # dwCaps3
|
||||
+ o32(0) # dwCaps4
|
||||
+ o32(0) # dwReserved2
|
||||
)
|
||||
if im.mode == "RGBA":
|
||||
r, g, b, a = im.split()
|
||||
im = Image.merge("RGBA", (a, r, g, b))
|
||||
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))])
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == b"DDS "
|
||||
|
||||
|
||||
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
||||
Image.register_save(DdsImageFile.format, _save)
|
||||
Image.register_extension(DdsImageFile.format, ".dds")
|
409
.venv/lib/python3.9/site-packages/PIL/EpsImagePlugin.py
Normal file
409
.venv/lib/python3.9/site-packages/PIL/EpsImagePlugin.py
Normal file
@ -0,0 +1,409 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i32le as i32
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||
|
||||
gs_windows_binary = None
|
||||
if sys.platform.startswith("win"):
|
||||
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
|
||||
|
||||
|
||||
def has_ghostscript():
|
||||
if gs_windows_binary:
|
||||
return True
|
||||
if not sys.platform.startswith("win"):
|
||||
try:
|
||||
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
||||
return True
|
||||
except OSError:
|
||||
# No Ghostscript
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def Ghostscript(tile, size, fp, scale=1):
|
||||
"""Render an image using Ghostscript"""
|
||||
|
||||
# Unpack decoder tile
|
||||
decoder, tile, offset, data = tile[0]
|
||||
length, bbox = data
|
||||
|
||||
# Hack to support hi-res rendering
|
||||
scale = int(scale) or 1
|
||||
# orig_size = size
|
||||
# orig_bbox = bbox
|
||||
size = (size[0] * scale, size[1] * scale)
|
||||
# resolution is dependent on bbox and size
|
||||
res = (
|
||||
72.0 * size[0] / (bbox[2] - bbox[0]),
|
||||
72.0 * size[1] / (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)
|
||||
|
||||
# Build Ghostscript command
|
||||
command = [
|
||||
"gs",
|
||||
"-q", # quiet mode
|
||||
"-g%dx%d" % size, # set output geometry (pixels)
|
||||
"-r%fx%f" % res, # set input DPI (dots per inch)
|
||||
"-dBATCH", # exit after processing
|
||||
"-dNOPAUSE", # don't pause between pages
|
||||
"-dSAFER", # safe mode
|
||||
"-sDEVICE=ppmraw", # ppm driver
|
||||
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",
|
||||
]
|
||||
|
||||
if gs_windows_binary is not None:
|
||||
if not gs_windows_binary:
|
||||
raise OSError("Unable to locate Ghostscript on paths")
|
||||
command[0] = gs_windows_binary
|
||||
|
||||
# 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)
|
||||
out_im = Image.open(outfile)
|
||||
out_im.load()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
if infile_temp:
|
||||
os.unlink(infile_temp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
im = out_im.im.copy()
|
||||
out_im.close()
|
||||
return im
|
||||
|
||||
|
||||
class PSFile:
|
||||
"""
|
||||
Wrapper for bytesio object that treats either CR or LF as end of line.
|
||||
"""
|
||||
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.char = None
|
||||
|
||||
def seek(self, offset, whence=io.SEEK_SET):
|
||||
self.char = None
|
||||
self.fp.seek(offset, whence)
|
||||
|
||||
def readline(self):
|
||||
s = [self.char or b""]
|
||||
self.char = None
|
||||
|
||||
c = self.fp.read(1)
|
||||
while (c not in b"\r\n") and len(c):
|
||||
s.append(c)
|
||||
c = self.fp.read(1)
|
||||
|
||||
self.char = self.fp.read(1)
|
||||
# line endings can be 1 or 2 of \r \n, in either order
|
||||
if self.char in b"\r\n":
|
||||
self.char = None
|
||||
|
||||
return b"".join(s).decode("latin-1")
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == 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):
|
||||
(length, offset) = self._find_offset(self.fp)
|
||||
|
||||
# Rewrap the open file pointer in something that will
|
||||
# convert line endings and decode to latin-1.
|
||||
fp = PSFile(self.fp)
|
||||
|
||||
# go to offset - start of "%!PS"
|
||||
fp.seek(offset)
|
||||
|
||||
box = None
|
||||
|
||||
self.mode = "RGB"
|
||||
self._size = 1, 1 # FIXME: huh?
|
||||
|
||||
#
|
||||
# Load EPS header
|
||||
|
||||
s_raw = fp.readline()
|
||||
s = s_raw.strip("\r\n")
|
||||
|
||||
while s_raw:
|
||||
if s:
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as e:
|
||||
raise SyntaxError("not an EPS file") from e
|
||||
|
||||
if m:
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [
|
||||
("eps", (0, 0) + self.size, offset, (length, box))
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
else:
|
||||
m = field.match(s)
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
||||
if k == "EndComments":
|
||||
break
|
||||
if k[:8] == "PS-Adobe":
|
||||
self.info[k[:8]] = 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:
|
||||
raise OSError("bad EPS header")
|
||||
|
||||
s_raw = fp.readline()
|
||||
s = s_raw.strip("\r\n")
|
||||
|
||||
if s and s[:1] != "%":
|
||||
break
|
||||
|
||||
#
|
||||
# Scan for an "ImageData" descriptor
|
||||
|
||||
while s[:1] == "%":
|
||||
|
||||
if len(s) > 255:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
if s[:11] == "%ImageData:":
|
||||
# Encoded bitmapped image.
|
||||
x, y, bi, mo = s[11:].split(None, 7)[:4]
|
||||
|
||||
if int(bi) != 8:
|
||||
break
|
||||
try:
|
||||
self.mode = self.mode_map[int(mo)]
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
self._size = int(x), int(y)
|
||||
return
|
||||
|
||||
s = fp.readline().strip("\r\n")
|
||||
if not s:
|
||||
break
|
||||
|
||||
if not box:
|
||||
raise OSError("cannot determine EPS bounding box")
|
||||
|
||||
def _find_offset(self, fp):
|
||||
|
||||
s = fp.read(160)
|
||||
|
||||
if s[:4] == b"%!PS":
|
||||
# for HEAD without binary preview
|
||||
fp.seek(0, io.SEEK_END)
|
||||
length = fp.tell()
|
||||
offset = 0
|
||||
elif i32(s, 0) == 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
|
||||
offset = i32(s, 4)
|
||||
length = i32(s, 8)
|
||||
else:
|
||||
raise SyntaxError("not an EPS file")
|
||||
|
||||
return (length, offset)
|
||||
|
||||
def load(self, scale=1):
|
||||
# Load EPS via Ghostscript
|
||||
if not self.tile:
|
||||
return
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale)
|
||||
self.mode = self.im.mode
|
||||
self._size = self.im.size
|
||||
self.tile = []
|
||||
|
||||
def load_seek(self, *args, **kwargs):
|
||||
# we can't incrementally load, so force ImageFile.parser to
|
||||
# use our custom load method by defining this method.
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _save(im, fp, filename, eps=1):
|
||||
"""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:
|
||||
raise ValueError("image mode is not supported")
|
||||
|
||||
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, [("eps", (0, 0) + im.size, 0, None)])
|
||||
|
||||
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")
|
331
.venv/lib/python3.9/site-packages/PIL/ExifTags.py
Normal file
331
.venv/lib/python3.9/site-packages/PIL/ExifTags.py
Normal file
@ -0,0 +1,331 @@
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
|
||||
|
||||
TAGS = {
|
||||
# possibly incomplete
|
||||
0x0001: "InteropIndex",
|
||||
0x000B: "ProcessingSoftware",
|
||||
0x00FE: "NewSubfileType",
|
||||
0x00FF: "SubfileType",
|
||||
0x0100: "ImageWidth",
|
||||
0x0101: "ImageLength",
|
||||
0x0102: "BitsPerSample",
|
||||
0x0103: "Compression",
|
||||
0x0106: "PhotometricInterpretation",
|
||||
0x0107: "Thresholding",
|
||||
0x0108: "CellWidth",
|
||||
0x0109: "CellLength",
|
||||
0x010A: "FillOrder",
|
||||
0x010D: "DocumentName",
|
||||
0x010E: "ImageDescription",
|
||||
0x010F: "Make",
|
||||
0x0110: "Model",
|
||||
0x0111: "StripOffsets",
|
||||
0x0112: "Orientation",
|
||||
0x0115: "SamplesPerPixel",
|
||||
0x0116: "RowsPerStrip",
|
||||
0x0117: "StripByteCounts",
|
||||
0x0118: "MinSampleValue",
|
||||
0x0119: "MaxSampleValue",
|
||||
0x011A: "XResolution",
|
||||
0x011B: "YResolution",
|
||||
0x011C: "PlanarConfiguration",
|
||||
0x011D: "PageName",
|
||||
0x0120: "FreeOffsets",
|
||||
0x0121: "FreeByteCounts",
|
||||
0x0122: "GrayResponseUnit",
|
||||
0x0123: "GrayResponseCurve",
|
||||
0x0124: "T4Options",
|
||||
0x0125: "T6Options",
|
||||
0x0128: "ResolutionUnit",
|
||||
0x0129: "PageNumber",
|
||||
0x012D: "TransferFunction",
|
||||
0x0131: "Software",
|
||||
0x0132: "DateTime",
|
||||
0x013B: "Artist",
|
||||
0x013C: "HostComputer",
|
||||
0x013D: "Predictor",
|
||||
0x013E: "WhitePoint",
|
||||
0x013F: "PrimaryChromaticities",
|
||||
0x0140: "ColorMap",
|
||||
0x0141: "HalftoneHints",
|
||||
0x0142: "TileWidth",
|
||||
0x0143: "TileLength",
|
||||
0x0144: "TileOffsets",
|
||||
0x0145: "TileByteCounts",
|
||||
0x014A: "SubIFDs",
|
||||
0x014C: "InkSet",
|
||||
0x014D: "InkNames",
|
||||
0x014E: "NumberOfInks",
|
||||
0x0150: "DotRange",
|
||||
0x0151: "TargetPrinter",
|
||||
0x0152: "ExtraSamples",
|
||||
0x0153: "SampleFormat",
|
||||
0x0154: "SMinSampleValue",
|
||||
0x0155: "SMaxSampleValue",
|
||||
0x0156: "TransferRange",
|
||||
0x0157: "ClipPath",
|
||||
0x0158: "XClipPathUnits",
|
||||
0x0159: "YClipPathUnits",
|
||||
0x015A: "Indexed",
|
||||
0x015B: "JPEGTables",
|
||||
0x015F: "OPIProxy",
|
||||
0x0200: "JPEGProc",
|
||||
0x0201: "JpegIFOffset",
|
||||
0x0202: "JpegIFByteCount",
|
||||
0x0203: "JpegRestartInterval",
|
||||
0x0205: "JpegLosslessPredictors",
|
||||
0x0206: "JpegPointTransforms",
|
||||
0x0207: "JpegQTables",
|
||||
0x0208: "JpegDCTables",
|
||||
0x0209: "JpegACTables",
|
||||
0x0211: "YCbCrCoefficients",
|
||||
0x0212: "YCbCrSubSampling",
|
||||
0x0213: "YCbCrPositioning",
|
||||
0x0214: "ReferenceBlackWhite",
|
||||
0x02BC: "XMLPacket",
|
||||
0x1000: "RelatedImageFileFormat",
|
||||
0x1001: "RelatedImageWidth",
|
||||
0x1002: "RelatedImageLength",
|
||||
0x4746: "Rating",
|
||||
0x4749: "RatingPercent",
|
||||
0x800D: "ImageID",
|
||||
0x828D: "CFARepeatPatternDim",
|
||||
0x828E: "CFAPattern",
|
||||
0x828F: "BatteryLevel",
|
||||
0x8298: "Copyright",
|
||||
0x829A: "ExposureTime",
|
||||
0x829D: "FNumber",
|
||||
0x83BB: "IPTCNAA",
|
||||
0x8649: "ImageResources",
|
||||
0x8769: "ExifOffset",
|
||||
0x8773: "InterColorProfile",
|
||||
0x8822: "ExposureProgram",
|
||||
0x8824: "SpectralSensitivity",
|
||||
0x8825: "GPSInfo",
|
||||
0x8827: "ISOSpeedRatings",
|
||||
0x8828: "OECF",
|
||||
0x8829: "Interlace",
|
||||
0x882A: "TimeZoneOffset",
|
||||
0x882B: "SelfTimerMode",
|
||||
0x8830: "SensitivityType",
|
||||
0x8831: "StandardOutputSensitivity",
|
||||
0x8832: "RecommendedExposureIndex",
|
||||
0x8833: "ISOSpeed",
|
||||
0x8834: "ISOSpeedLatitudeyyy",
|
||||
0x8835: "ISOSpeedLatitudezzz",
|
||||
0x9000: "ExifVersion",
|
||||
0x9003: "DateTimeOriginal",
|
||||
0x9004: "DateTimeDigitized",
|
||||
0x9010: "OffsetTime",
|
||||
0x9011: "OffsetTimeOriginal",
|
||||
0x9012: "OffsetTimeDigitized",
|
||||
0x9101: "ComponentsConfiguration",
|
||||
0x9102: "CompressedBitsPerPixel",
|
||||
0x9201: "ShutterSpeedValue",
|
||||
0x9202: "ApertureValue",
|
||||
0x9203: "BrightnessValue",
|
||||
0x9204: "ExposureBiasValue",
|
||||
0x9205: "MaxApertureValue",
|
||||
0x9206: "SubjectDistance",
|
||||
0x9207: "MeteringMode",
|
||||
0x9208: "LightSource",
|
||||
0x9209: "Flash",
|
||||
0x920A: "FocalLength",
|
||||
0x920B: "FlashEnergy",
|
||||
0x920C: "SpatialFrequencyResponse",
|
||||
0x920D: "Noise",
|
||||
0x9211: "ImageNumber",
|
||||
0x9212: "SecurityClassification",
|
||||
0x9213: "ImageHistory",
|
||||
0x9214: "SubjectLocation",
|
||||
0x9215: "ExposureIndex",
|
||||
0x9216: "TIFF/EPStandardID",
|
||||
0x927C: "MakerNote",
|
||||
0x9286: "UserComment",
|
||||
0x9290: "SubsecTime",
|
||||
0x9291: "SubsecTimeOriginal",
|
||||
0x9292: "SubsecTimeDigitized",
|
||||
0x9400: "AmbientTemperature",
|
||||
0x9401: "Humidity",
|
||||
0x9402: "Pressure",
|
||||
0x9403: "WaterDepth",
|
||||
0x9404: "Acceleration",
|
||||
0x9405: "CameraElevationAngle",
|
||||
0x9C9B: "XPTitle",
|
||||
0x9C9C: "XPComment",
|
||||
0x9C9D: "XPAuthor",
|
||||
0x9C9E: "XPKeywords",
|
||||
0x9C9F: "XPSubject",
|
||||
0xA000: "FlashPixVersion",
|
||||
0xA001: "ColorSpace",
|
||||
0xA002: "ExifImageWidth",
|
||||
0xA003: "ExifImageHeight",
|
||||
0xA004: "RelatedSoundFile",
|
||||
0xA005: "ExifInteroperabilityOffset",
|
||||
0xA20B: "FlashEnergy",
|
||||
0xA20C: "SpatialFrequencyResponse",
|
||||
0xA20E: "FocalPlaneXResolution",
|
||||
0xA20F: "FocalPlaneYResolution",
|
||||
0xA210: "FocalPlaneResolutionUnit",
|
||||
0xA214: "SubjectLocation",
|
||||
0xA215: "ExposureIndex",
|
||||
0xA217: "SensingMethod",
|
||||
0xA300: "FileSource",
|
||||
0xA301: "SceneType",
|
||||
0xA302: "CFAPattern",
|
||||
0xA401: "CustomRendered",
|
||||
0xA402: "ExposureMode",
|
||||
0xA403: "WhiteBalance",
|
||||
0xA404: "DigitalZoomRatio",
|
||||
0xA405: "FocalLengthIn35mmFilm",
|
||||
0xA406: "SceneCaptureType",
|
||||
0xA407: "GainControl",
|
||||
0xA408: "Contrast",
|
||||
0xA409: "Saturation",
|
||||
0xA40A: "Sharpness",
|
||||
0xA40B: "DeviceSettingDescription",
|
||||
0xA40C: "SubjectDistanceRange",
|
||||
0xA420: "ImageUniqueID",
|
||||
0xA430: "CameraOwnerName",
|
||||
0xA431: "BodySerialNumber",
|
||||
0xA432: "LensSpecification",
|
||||
0xA433: "LensMake",
|
||||
0xA434: "LensModel",
|
||||
0xA435: "LensSerialNumber",
|
||||
0xA460: "CompositeImage",
|
||||
0xA461: "CompositeImageCount",
|
||||
0xA462: "CompositeImageExposureTimes",
|
||||
0xA500: "Gamma",
|
||||
0xC4A5: "PrintImageMatching",
|
||||
0xC612: "DNGVersion",
|
||||
0xC613: "DNGBackwardVersion",
|
||||
0xC614: "UniqueCameraModel",
|
||||
0xC615: "LocalizedCameraModel",
|
||||
0xC616: "CFAPlaneColor",
|
||||
0xC617: "CFALayout",
|
||||
0xC618: "LinearizationTable",
|
||||
0xC619: "BlackLevelRepeatDim",
|
||||
0xC61A: "BlackLevel",
|
||||
0xC61B: "BlackLevelDeltaH",
|
||||
0xC61C: "BlackLevelDeltaV",
|
||||
0xC61D: "WhiteLevel",
|
||||
0xC61E: "DefaultScale",
|
||||
0xC61F: "DefaultCropOrigin",
|
||||
0xC620: "DefaultCropSize",
|
||||
0xC621: "ColorMatrix1",
|
||||
0xC622: "ColorMatrix2",
|
||||
0xC623: "CameraCalibration1",
|
||||
0xC624: "CameraCalibration2",
|
||||
0xC625: "ReductionMatrix1",
|
||||
0xC626: "ReductionMatrix2",
|
||||
0xC627: "AnalogBalance",
|
||||
0xC628: "AsShotNeutral",
|
||||
0xC629: "AsShotWhiteXY",
|
||||
0xC62A: "BaselineExposure",
|
||||
0xC62B: "BaselineNoise",
|
||||
0xC62C: "BaselineSharpness",
|
||||
0xC62D: "BayerGreenSplit",
|
||||
0xC62E: "LinearResponseLimit",
|
||||
0xC62F: "CameraSerialNumber",
|
||||
0xC630: "LensInfo",
|
||||
0xC631: "ChromaBlurRadius",
|
||||
0xC632: "AntiAliasStrength",
|
||||
0xC633: "ShadowScale",
|
||||
0xC634: "DNGPrivateData",
|
||||
0xC635: "MakerNoteSafety",
|
||||
0xC65A: "CalibrationIlluminant1",
|
||||
0xC65B: "CalibrationIlluminant2",
|
||||
0xC65C: "BestQualityScale",
|
||||
0xC65D: "RawDataUniqueID",
|
||||
0xC68B: "OriginalRawFileName",
|
||||
0xC68C: "OriginalRawFileData",
|
||||
0xC68D: "ActiveArea",
|
||||
0xC68E: "MaskedAreas",
|
||||
0xC68F: "AsShotICCProfile",
|
||||
0xC690: "AsShotPreProfileMatrix",
|
||||
0xC691: "CurrentICCProfile",
|
||||
0xC692: "CurrentPreProfileMatrix",
|
||||
0xC6BF: "ColorimetricReference",
|
||||
0xC6F3: "CameraCalibrationSignature",
|
||||
0xC6F4: "ProfileCalibrationSignature",
|
||||
0xC6F6: "AsShotProfileName",
|
||||
0xC6F7: "NoiseReductionApplied",
|
||||
0xC6F8: "ProfileName",
|
||||
0xC6F9: "ProfileHueSatMapDims",
|
||||
0xC6FA: "ProfileHueSatMapData1",
|
||||
0xC6FB: "ProfileHueSatMapData2",
|
||||
0xC6FC: "ProfileToneCurve",
|
||||
0xC6FD: "ProfileEmbedPolicy",
|
||||
0xC6FE: "ProfileCopyright",
|
||||
0xC714: "ForwardMatrix1",
|
||||
0xC715: "ForwardMatrix2",
|
||||
0xC716: "PreviewApplicationName",
|
||||
0xC717: "PreviewApplicationVersion",
|
||||
0xC718: "PreviewSettingsName",
|
||||
0xC719: "PreviewSettingsDigest",
|
||||
0xC71A: "PreviewColorSpace",
|
||||
0xC71B: "PreviewDateTime",
|
||||
0xC71C: "RawImageDigest",
|
||||
0xC71D: "OriginalRawFileDigest",
|
||||
0xC71E: "SubTileBlockSize",
|
||||
0xC71F: "RowInterleaveFactor",
|
||||
0xC725: "ProfileLookTableDims",
|
||||
0xC726: "ProfileLookTableData",
|
||||
0xC740: "OpcodeList1",
|
||||
0xC741: "OpcodeList2",
|
||||
0xC74E: "OpcodeList3",
|
||||
0xC761: "NoiseProfile",
|
||||
}
|
||||
"""Maps EXIF tags to tag names."""
|
||||
|
||||
|
||||
GPSTAGS = {
|
||||
0: "GPSVersionID",
|
||||
1: "GPSLatitudeRef",
|
||||
2: "GPSLatitude",
|
||||
3: "GPSLongitudeRef",
|
||||
4: "GPSLongitude",
|
||||
5: "GPSAltitudeRef",
|
||||
6: "GPSAltitude",
|
||||
7: "GPSTimeStamp",
|
||||
8: "GPSSatellites",
|
||||
9: "GPSStatus",
|
||||
10: "GPSMeasureMode",
|
||||
11: "GPSDOP",
|
||||
12: "GPSSpeedRef",
|
||||
13: "GPSSpeed",
|
||||
14: "GPSTrackRef",
|
||||
15: "GPSTrack",
|
||||
16: "GPSImgDirectionRef",
|
||||
17: "GPSImgDirection",
|
||||
18: "GPSMapDatum",
|
||||
19: "GPSDestLatitudeRef",
|
||||
20: "GPSDestLatitude",
|
||||
21: "GPSDestLongitudeRef",
|
||||
22: "GPSDestLongitude",
|
||||
23: "GPSDestBearingRef",
|
||||
24: "GPSDestBearing",
|
||||
25: "GPSDestDistanceRef",
|
||||
26: "GPSDestDistance",
|
||||
27: "GPSProcessingMethod",
|
||||
28: "GPSAreaInformation",
|
||||
29: "GPSDateStamp",
|
||||
30: "GPSDifferential",
|
||||
31: "GPSHPositioningError",
|
||||
}
|
||||
"""Maps EXIF GPS tags to tag names."""
|
100
.venv/lib/python3.9/site-packages/PIL/FitsStubImagePlugin.py
Normal file
100
.venv/lib/python3.9/site-packages/PIL/FitsStubImagePlugin.py
Normal file
@ -0,0 +1,100 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# FITS stub adapter
|
||||
#
|
||||
# Copyright (c) 1998-2003 by Fredrik Lundh
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific FITS image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:6] == b"SIMPLE"
|
||||
|
||||
|
||||
class FITSStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "FITS"
|
||||
format_description = "FITS"
|
||||
|
||||
def _open(self):
|
||||
offset = self.fp.tell()
|
||||
|
||||
headers = {}
|
||||
while True:
|
||||
header = self.fp.read(80)
|
||||
if not header:
|
||||
raise OSError("Truncated FITS file")
|
||||
keyword = header[:8].strip()
|
||||
if keyword == b"END":
|
||||
break
|
||||
value = header[8:].strip()
|
||||
if value.startswith(b"="):
|
||||
value = value[1:].strip()
|
||||
if not headers and (not _accept(keyword) or value != b"T"):
|
||||
raise SyntaxError("Not a FITS file")
|
||||
headers[keyword] = value
|
||||
|
||||
naxis = int(headers[b"NAXIS"])
|
||||
if naxis == 0:
|
||||
raise ValueError("No image data")
|
||||
elif naxis == 1:
|
||||
self._size = 1, int(headers[b"NAXIS1"])
|
||||
else:
|
||||
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
|
||||
|
||||
number_of_bits = int(headers[b"BITPIX"])
|
||||
if number_of_bits == 8:
|
||||
self.mode = "L"
|
||||
elif number_of_bits == 16:
|
||||
self.mode = "I"
|
||||
# rawmode = "I;16S"
|
||||
elif number_of_bits == 32:
|
||||
self.mode = "I"
|
||||
elif number_of_bits in (-32, -64):
|
||||
self.mode = "F"
|
||||
# rawmode = "F" if number_of_bits == -32 else "F;64F"
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
return _handler
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise OSError("FITS save handler not installed")
|
||||
_handler.save(im, fp, filename)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
|
||||
Image.register_save(FITSStubImageFile.format, _save)
|
||||
|
||||
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])
|
171
.venv/lib/python3.9/site-packages/PIL/FliImagePlugin.py
Normal file
171
.venv/lib/python3.9/site-packages/PIL/FliImagePlugin.py
Normal file
@ -0,0 +1,171 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
from ._binary import o8
|
||||
|
||||
#
|
||||
# decoder
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||
|
||||
|
||||
##
|
||||
# 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):
|
||||
|
||||
# HEAD
|
||||
s = self.fp.read(128)
|
||||
if not (
|
||||
_accept(s)
|
||||
and i16(s, 14) in [0, 3] # flags
|
||||
and s[20:22] == b"\x00\x00" # reserved
|
||||
):
|
||||
raise SyntaxError("not an FLI/FLC file")
|
||||
|
||||
# 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.__offset = self.__offset + i32(s)
|
||||
s = self.fp.read(16)
|
||||
|
||||
if i16(s, 4) == 0xF1FA:
|
||||
# look for palette chunk
|
||||
s = self.fp.read(6)
|
||||
if i16(s, 4) == 11:
|
||||
self._palette(palette, 2)
|
||||
elif i16(s, 4) == 4:
|
||||
self._palette(palette, 0)
|
||||
|
||||
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
|
||||
self.palette = ImagePalette.raw("RGB", b"".join(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, shift):
|
||||
# load palette
|
||||
|
||||
i = 0
|
||||
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):
|
||||
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):
|
||||
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:
|
||||
raise ValueError(f"cannot seek to frame {frame}")
|
||||
self.__frame = frame
|
||||
|
||||
# move to next frame
|
||||
self.fp = self.__fp
|
||||
self.fp.seek(self.__offset)
|
||||
|
||||
s = self.fp.read(4)
|
||||
if not s:
|
||||
raise EOFError
|
||||
|
||||
framesize = i32(s)
|
||||
|
||||
self.decodermaxblock = framesize
|
||||
self.tile = [("fli", (0, 0) + self.size, self.__offset, None)]
|
||||
|
||||
self.__offset += framesize
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(FliImageFile.format, FliImageFile, _accept)
|
||||
|
||||
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])
|
111
.venv/lib/python3.9/site-packages/PIL/FontFile.py
Normal file
111
.venv/lib/python3.9/site-packages/PIL/FontFile.py
Normal file
@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from . import Image, _binary
|
||||
|
||||
WIDTH = 800
|
||||
|
||||
|
||||
def puti16(fp, values):
|
||||
"""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 = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.info = {}
|
||||
self.glyph = [None] * 256
|
||||
|
||||
def __getitem__(self, ix):
|
||||
return self.glyph[ix]
|
||||
|
||||
def compile(self):
|
||||
"""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:
|
||||
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 = [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]
|
||||
# yy = src[3] - src[1]
|
||||
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 save(self, filename):
|
||||
"""Save font"""
|
||||
|
||||
self.compile()
|
||||
|
||||
# font data
|
||||
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")
|
||||
for id in range(256):
|
||||
m = self.metrics[id]
|
||||
if not m:
|
||||
puti16(fp, [0] * 10)
|
||||
else:
|
||||
puti16(fp, m[0] + m[1] + m[2])
|
242
.venv/lib/python3.9/site-packages/PIL/FpxImagePlugin.py
Normal file
242
.venv/lib/python3.9/site-packages/PIL/FpxImagePlugin.py
Normal file
@ -0,0 +1,242 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
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):
|
||||
return prefix[:8] == olefile.MAGIC
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for the FlashPix images.
|
||||
|
||||
|
||||
class FpxImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "FPX"
|
||||
format_description = "FlashPix"
|
||||
|
||||
def _open(self):
|
||||
#
|
||||
# read the OLE directory and see if this is a likely
|
||||
# to be a FlashPix file
|
||||
|
||||
try:
|
||||
self.ole = olefile.OleFileIO(self.fp)
|
||||
except OSError as e:
|
||||
raise SyntaxError("not an FPX file; invalid OLE file") from e
|
||||
|
||||
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
|
||||
raise SyntaxError("not an FPX file; bad root CLSID")
|
||||
|
||||
self._open_index(1)
|
||||
|
||||
def _open_index(self, index=1):
|
||||
#
|
||||
# get the Image Contents Property Set
|
||||
|
||||
prop = self.ole.getproperties(
|
||||
[f"Data Object Store {index:06d}", "\005Image Contents"]
|
||||
)
|
||||
|
||||
# size (highest resolution)
|
||||
|
||||
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]
|
||||
|
||||
colors = []
|
||||
bands = i32(s, 4)
|
||||
if bands > 4:
|
||||
raise OSError("Invalid number of bands")
|
||||
for i in range(bands):
|
||||
# note: for now, we ignore the "uncalibrated" flag
|
||||
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
|
||||
|
||||
self.mode, self.rawmode = MODES[tuple(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=1, subimage=0):
|
||||
#
|
||||
# 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)
|
||||
tilesize = i32(s, 16), i32(s, 20)
|
||||
# channels = i32(s, 24)
|
||||
offset = i32(s, 28)
|
||||
length = i32(s, 32)
|
||||
|
||||
if size != self.size:
|
||||
raise OSError("subimage mismatch")
|
||||
|
||||
# get tile descriptors
|
||||
fp.seek(28 + offset)
|
||||
s = fp.read(i32(s, 12) * length)
|
||||
|
||||
x = y = 0
|
||||
xsize, ysize = size
|
||||
xtile, ytile = tilesize
|
||||
self.tile = []
|
||||
|
||||
for i in range(0, len(s), length):
|
||||
|
||||
compression = i32(s, i + 8)
|
||||
|
||||
if compression == 0:
|
||||
self.tile.append(
|
||||
(
|
||||
"raw",
|
||||
(x, y, x + xtile, y + ytile),
|
||||
i32(s, i) + 28,
|
||||
(self.rawmode),
|
||||
)
|
||||
)
|
||||
|
||||
elif compression == 1:
|
||||
|
||||
# FIXME: the fill decoder is not implemented
|
||||
self.tile.append(
|
||||
(
|
||||
"fill",
|
||||
(x, y, x + xtile, y + ytile),
|
||||
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(
|
||||
(
|
||||
"jpeg",
|
||||
(x, y, x + xtile, y + ytile),
|
||||
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:
|
||||
raise OSError("unknown/invalid compression")
|
||||
|
||||
x = x + xtile
|
||||
if x >= xsize:
|
||||
x, y = 0, y + ytile
|
||||
if y >= ysize:
|
||||
break # isn't really required
|
||||
|
||||
self.stream = stream
|
||||
self.fp = None
|
||||
|
||||
def load(self):
|
||||
|
||||
if not self.fp:
|
||||
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
|
||||
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
|
||||
|
||||
Image.register_extension(FpxImageFile.format, ".fpx")
|
106
.venv/lib/python3.9/site-packages/PIL/FtexImagePlugin.py
Normal file
106
.venv/lib/python3.9/site-packages/PIL/FtexImagePlugin.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
MAGIC = b"FTEX"
|
||||
FORMAT_DXT1 = 0
|
||||
FORMAT_UNCOMPRESSED = 1
|
||||
|
||||
|
||||
class FtexImageFile(ImageFile.ImageFile):
|
||||
format = "FTEX"
|
||||
format_description = "Texture File Format (IW2:EOC)"
|
||||
|
||||
def _open(self):
|
||||
struct.unpack("<I", self.fp.read(4)) # magic
|
||||
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))
|
||||
|
||||
self.mode = "RGB"
|
||||
|
||||
# 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 = [("bcn", (0, 0) + self.size, 0, (1))]
|
||||
elif format == FORMAT_UNCOMPRESSED:
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
|
||||
else:
|
||||
raise ValueError(f"Invalid texture compression format: {repr(format)}")
|
||||
|
||||
self.fp.close()
|
||||
self.fp = BytesIO(data)
|
||||
|
||||
def load_seek(self, pos):
|
||||
pass
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == MAGIC
|
||||
|
||||
|
||||
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
|
||||
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])
|
100
.venv/lib/python3.9/site-packages/PIL/GbrImagePlugin.py
Normal file
100
.venv/lib/python3.9/site-packages/PIL/GbrImagePlugin.py
Normal file
@ -0,0 +1,100 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
from ._binary import i32be as i32
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
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):
|
||||
header_size = i32(self.fp.read(4))
|
||||
version = i32(self.fp.read(4))
|
||||
if header_size < 20:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
if version not in (1, 2):
|
||||
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
|
||||
|
||||
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:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
if color_depth not in (1, 4):
|
||||
raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}")
|
||||
|
||||
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":
|
||||
raise SyntaxError("not a GIMP brush, bad magic number")
|
||||
self.info["spacing"] = i32(self.fp.read(4))
|
||||
|
||||
comment = self.fp.read(comment_length)[:-1]
|
||||
|
||||
if color_depth == 1:
|
||||
self.mode = "L"
|
||||
else:
|
||||
self.mode = "RGBA"
|
||||
|
||||
self._size = width, height
|
||||
|
||||
self.info["comment"] = comment
|
||||
|
||||
# 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):
|
||||
if self.im:
|
||||
# Already loaded
|
||||
return
|
||||
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
self.frombytes(self.fp.read(self._data_size))
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
|
||||
Image.register_extension(GbrImageFile.format, ".gbr")
|
90
.venv/lib/python3.9/site-packages/PIL/GdImageFile.py
Normal file
90
.venv/lib/python3.9/site-packages/PIL/GdImageFile.py
Normal file
@ -0,0 +1,90 @@
|
||||
#
|
||||
# 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 . import ImageFile, ImagePalette, UnidentifiedImageError
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
|
||||
|
||||
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):
|
||||
|
||||
# Header
|
||||
s = self.fp.read(1037)
|
||||
|
||||
if not i16(s) in [65534, 65535]:
|
||||
raise SyntaxError("Not a valid GD 2.x .gd file")
|
||||
|
||||
self.mode = "L" # FIXME: "P"
|
||||
self._size = i16(s, 2), i16(s, 4)
|
||||
|
||||
trueColor = s[6]
|
||||
trueColorOffset = 2 if trueColor else 0
|
||||
|
||||
# transparency index
|
||||
tindex = i32(s, 7 + trueColorOffset)
|
||||
if tindex < 256:
|
||||
self.info["transparency"] = tindex
|
||||
|
||||
self.palette = ImagePalette.raw(
|
||||
"XBGR", s[7 + trueColorOffset + 4 : 7 + trueColorOffset + 4 + 256 * 4]
|
||||
)
|
||||
|
||||
self.tile = [
|
||||
("raw", (0, 0) + self.size, 7 + trueColorOffset + 4 + 256 * 4, ("L", 0, 1))
|
||||
]
|
||||
|
||||
|
||||
def open(fp, mode="r"):
|
||||
"""
|
||||
Load texture from a GD image file.
|
||||
|
||||
:param filename: 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":
|
||||
raise ValueError("bad mode")
|
||||
|
||||
try:
|
||||
return GdImageFile(fp)
|
||||
except SyntaxError as e:
|
||||
raise UnidentifiedImageError("cannot identify this image file") from e
|
918
.venv/lib/python3.9/site-packages/PIL/GifImagePlugin.py
Normal file
918
.venv/lib/python3.9/site-packages/PIL/GifImagePlugin.py
Normal file
@ -0,0 +1,918 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import o8
|
||||
from ._binary import o16le as o16
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Identify/read GIF files
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:6] in [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):
|
||||
s = self.fp.read(1)
|
||||
if s and s[0]:
|
||||
return self.fp.read(s[0])
|
||||
return None
|
||||
|
||||
def _open(self):
|
||||
|
||||
# Screen
|
||||
s = self.fp.read(13)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a GIF file")
|
||||
|
||||
self.info["version"] = s[:6]
|
||||
self._size = i16(s, 6), i16(s, 8)
|
||||
self.tile = []
|
||||
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)
|
||||
for i in range(0, len(p), 3):
|
||||
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
||||
p = ImagePalette.raw("RGB", p)
|
||||
self.global_palette = self.palette = p
|
||||
break
|
||||
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
self.__rewind = self.fp.tell()
|
||||
self._n_frames = None
|
||||
self._is_animated = None
|
||||
self._seek(0) # get ready to read first frame
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
if self._n_frames is None:
|
||||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self.seek(self.tell() + 1)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self.seek(current)
|
||||
return self._n_frames
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
if self._n_frames is not None:
|
||||
self._is_animated = self._n_frames != 1
|
||||
else:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
if frame < self.__frame:
|
||||
if frame != 0:
|
||||
self.im = None
|
||||
self._seek(0)
|
||||
|
||||
last_frame = self.__frame
|
||||
for f in range(self.__frame + 1, frame + 1):
|
||||
try:
|
||||
self._seek(f)
|
||||
except EOFError as e:
|
||||
self.seek(last_frame)
|
||||
raise EOFError("no more images in GIF file") from e
|
||||
|
||||
def _seek(self, frame):
|
||||
|
||||
if frame == 0:
|
||||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.disposal_method = 0
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
if self.tile:
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError(f"cannot seek to frame {frame}")
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
self.fp = self.__fp
|
||||
if self.__offset:
|
||||
# backup to last frame
|
||||
self.fp.seek(self.__offset)
|
||||
while self.data():
|
||||
pass
|
||||
self.__offset = 0
|
||||
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
from copy import copy
|
||||
|
||||
self.palette = copy(self.global_palette)
|
||||
|
||||
info = {}
|
||||
frame_transparency = None
|
||||
interlace = None
|
||||
while True:
|
||||
|
||||
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:
|
||||
#
|
||||
# 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
|
||||
#
|
||||
while block:
|
||||
if "comment" in info:
|
||||
info["comment"] += block
|
||||
else:
|
||||
info["comment"] = block
|
||||
block = self.data()
|
||||
continue
|
||||
elif s[0] == 255:
|
||||
#
|
||||
# application extension
|
||||
#
|
||||
info["extension"] = block, self.fp.tell()
|
||||
if block[:11] == b"NETSCAPE2.0":
|
||||
block = self.data()
|
||||
if len(block) >= 3 and block[0] == 1:
|
||||
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]:
|
||||
self._size = max(x1, self.size[0]), max(y1, self.size[1])
|
||||
self.dispose_extent = x0, y0, x1, y1
|
||||
flags = s[8]
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
||||
if flags & 128:
|
||||
bits = (flags & 7) + 1
|
||||
self.palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
|
||||
|
||||
# image data
|
||||
bits = self.fp.read(1)[0]
|
||||
self.__offset = self.fp.tell()
|
||||
break
|
||||
|
||||
else:
|
||||
pass
|
||||
# raise OSError, "illegal GIF tag `%x`" % s[0]
|
||||
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
self.dispose = None
|
||||
elif 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
|
||||
color = (
|
||||
frame_transparency
|
||||
if frame_transparency is not None
|
||||
else self.info.get("background", 0)
|
||||
)
|
||||
self.dispose = Image.core.fill("P", dispose_size, color)
|
||||
else:
|
||||
# replace with previous contents
|
||||
if self.im:
|
||||
# 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)
|
||||
self.dispose = Image.core.fill(
|
||||
"P", dispose_size, frame_transparency
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if interlace is not None:
|
||||
transparency = -1
|
||||
if frame_transparency is not None:
|
||||
if frame == 0:
|
||||
self.info["transparency"] = frame_transparency
|
||||
else:
|
||||
transparency = frame_transparency
|
||||
self.tile = [
|
||||
(
|
||||
"gif",
|
||||
(x0, y0, x1, y1),
|
||||
self.__offset,
|
||||
(bits, interlace, transparency),
|
||||
)
|
||||
]
|
||||
else:
|
||||
# self.__fp = None
|
||||
raise EOFError
|
||||
|
||||
for k in ["duration", "comment", "extension", "loop"]:
|
||||
if k in info:
|
||||
self.info[k] = info[k]
|
||||
elif k in self.info:
|
||||
del self.info[k]
|
||||
|
||||
self.mode = "L"
|
||||
if self.palette:
|
||||
self.mode = "P"
|
||||
|
||||
def load_prepare(self):
|
||||
if not self.im and "transparency" in self.info:
|
||||
self.im = Image.core.fill(self.mode, self.size, self.info["transparency"])
|
||||
|
||||
super(GifImageFile, self).load_prepare()
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Write GIF files
|
||||
|
||||
|
||||
RAWMODE = {"1": "L", "L": "L", "P": "P"}
|
||||
|
||||
|
||||
def _normalize_mode(im, initial_call=False):
|
||||
"""
|
||||
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.
|
||||
|
||||
UNDONE: What is the point of mucking with the initial call palette, for
|
||||
an image that shouldn't have a palette, or it would be a mode 'P' and
|
||||
get returned in the RAWMODE clause.
|
||||
|
||||
:param im: Image object
|
||||
:param initial_call: Default false, set to true for a single frame.
|
||||
:returns: Image object
|
||||
"""
|
||||
if im.mode in RAWMODE:
|
||||
im.load()
|
||||
return im
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
if initial_call:
|
||||
palette_size = 256
|
||||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
|
||||
else:
|
||||
return im.convert("P")
|
||||
return im.convert("L")
|
||||
|
||||
|
||||
def _normalize_palette(im, palette, info):
|
||||
"""
|
||||
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(
|
||||
itertools.chain.from_iterable(
|
||||
zip(
|
||||
palette.palette[:256],
|
||||
palette.palette[256:512],
|
||||
palette.palette[512:768],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if im.mode == "P":
|
||||
if not source_palette:
|
||||
source_palette = im.im.getpalette("RGB")[:768]
|
||||
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)
|
||||
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
if used_palette_colors is not None:
|
||||
return im.remap_palette(used_palette_colors, source_palette)
|
||||
|
||||
im.palette.palette = source_palette
|
||||
return im
|
||||
|
||||
|
||||
def _write_single_frame(im, fp, palette):
|
||||
im_out = _normalize_mode(im, True)
|
||||
for k, v in im_out.info.items():
|
||||
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, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])
|
||||
|
||||
fp.write(b"\0") # end of image data
|
||||
|
||||
|
||||
def _write_multiple_frames(im, fp, palette):
|
||||
|
||||
duration = im.encoderinfo.get("duration", im.info.get("duration"))
|
||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
||||
|
||||
im_frames = []
|
||||
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():
|
||||
im.encoderinfo.setdefault(k, v)
|
||||
im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
|
||||
|
||||
encoderinfo = im.encoderinfo.copy()
|
||||
if isinstance(duration, (list, tuple)):
|
||||
encoderinfo["duration"] = duration[frame_count]
|
||||
if isinstance(disposal, (list, tuple)):
|
||||
encoderinfo["disposal"] = disposal[frame_count]
|
||||
frame_count += 1
|
||||
|
||||
if im_frames:
|
||||
# delta frame
|
||||
previous = im_frames[-1]
|
||||
if encoderinfo.get("disposal") == 2:
|
||||
if background_im is None:
|
||||
color = im.encoderinfo.get(
|
||||
"transparency", im.info.get("transparency", (0, 0, 0))
|
||||
)
|
||||
background = _get_background(im_frame, color)
|
||||
background_im = Image.new("P", im_frame.size, background)
|
||||
background_im.putpalette(im_frames[0]["im"].palette)
|
||||
base_im = background_im
|
||||
else:
|
||||
base_im = previous["im"]
|
||||
if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
|
||||
delta = ImageChops.subtract_modulo(im_frame, base_im)
|
||||
else:
|
||||
delta = ImageChops.subtract_modulo(
|
||||
im_frame.convert("RGB"), base_im.convert("RGB")
|
||||
)
|
||||
bbox = delta.getbbox()
|
||||
if not bbox:
|
||||
# This frame is identical to the previous frame
|
||||
if duration:
|
||||
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
||||
continue
|
||||
else:
|
||||
bbox = None
|
||||
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
|
||||
|
||||
if len(im_frames) > 1:
|
||||
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
|
||||
frame_data["encoderinfo"]["include_color_table"] = True
|
||||
|
||||
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
|
||||
elif "duration" in im.encoderinfo and isinstance(
|
||||
im.encoderinfo["duration"], (list, tuple)
|
||||
):
|
||||
# Since multiple frames will not be written, add together the frame durations
|
||||
im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
|
||||
|
||||
|
||||
def _save_all(im, fp, filename):
|
||||
_save(im, fp, filename, save_all=True)
|
||||
|
||||
|
||||
def _save(im, fp, filename, save_all=False):
|
||||
# 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["optimize"] = im.encoderinfo.get("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):
|
||||
interlace = im.encoderinfo.get("interlace", 1)
|
||||
|
||||
# workaround for @PIL153
|
||||
if min(im.size) < 16:
|
||||
interlace = 0
|
||||
|
||||
return interlace
|
||||
|
||||
|
||||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
transparency = int(transparency)
|
||||
# optimize the block away if transparent color is not used
|
||||
transparent_color_exists = True
|
||||
|
||||
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
||||
if used_palette_colors is not None:
|
||||
# adjust the transparency index after optimize
|
||||
try:
|
||||
transparency = used_palette_colors.index(transparency)
|
||||
except ValueError:
|
||||
transparent_color_exists = False
|
||||
|
||||
if "duration" in im.encoderinfo:
|
||||
duration = int(im.encoderinfo["duration"] / 10)
|
||||
else:
|
||||
duration = 0
|
||||
|
||||
disposal = int(im.encoderinfo.get("disposal", 0))
|
||||
|
||||
if transparent_color_exists or duration != 0 or disposal:
|
||||
packed_flag = 1 if transparent_color_exists else 0
|
||||
packed_flag |= disposal << 2
|
||||
if not transparent_color_exists:
|
||||
transparency = 0
|
||||
|
||||
fp.write(
|
||||
b"!"
|
||||
+ o8(249) # extension intro
|
||||
+ o8(4) # length
|
||||
+ o8(packed_flag) # packed fields
|
||||
+ o16(duration) # duration
|
||||
+ o8(transparency) # transparency index
|
||||
+ o8(0)
|
||||
)
|
||||
|
||||
if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]):
|
||||
fp.write(b"!" + o8(254)) # extension intro
|
||||
comment = im.encoderinfo["comment"]
|
||||
if isinstance(comment, str):
|
||||
comment = comment.encode()
|
||||
for i in range(0, len(comment), 255):
|
||||
subblock = comment[i : i + 255]
|
||||
fp.write(o8(len(subblock)) + subblock)
|
||||
fp.write(o8(0))
|
||||
if "loop" in im.encoderinfo:
|
||||
number_of_loops = im.encoderinfo["loop"]
|
||||
fp.write(
|
||||
b"!"
|
||||
+ o8(255) # extension intro
|
||||
+ o8(11)
|
||||
+ b"NETSCAPE2.0"
|
||||
+ o8(3)
|
||||
+ o8(1)
|
||||
+ o16(number_of_loops) # number of loops
|
||||
+ 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, fp, filename):
|
||||
|
||||
# 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
|
||||
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, info):
|
||||
"""
|
||||
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", 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 (
|
||||
len(used_palette_colors) <= 128
|
||||
and max(used_palette_colors) > len(used_palette_colors)
|
||||
):
|
||||
return used_palette_colors
|
||||
|
||||
|
||||
def _get_color_table_size(palette_bytes):
|
||||
# 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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Gets the palette for inclusion in the gif header
|
||||
|
||||
:param im: Image object
|
||||
:returns: Bytes, len<=768 suitable for inclusion in gif header
|
||||
"""
|
||||
return im.palette.palette
|
||||
|
||||
|
||||
def _get_background(im, infoBackground):
|
||||
background = 0
|
||||
if infoBackground:
|
||||
background = infoBackground
|
||||
if isinstance(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
|
||||
try:
|
||||
background = im.palette.getcolor(background, im)
|
||||
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 the background color
|
||||
return 0
|
||||
else:
|
||||
raise
|
||||
return background
|
||||
|
||||
|
||||
def _get_global_header(im, info):
|
||||
"""Return a list of strings representing a GIF header"""
|
||||
|
||||
# Header Block
|
||||
# http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
|
||||
version = b"87a"
|
||||
for extensionKey in ["transparency", "duration", "loop", "comment"]:
|
||||
if info and extensionKey in info:
|
||||
if (extensionKey == "duration" and info[extensionKey] == 0) or (
|
||||
extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255)
|
||||
):
|
||||
continue
|
||||
version = b"89a"
|
||||
break
|
||||
else:
|
||||
if im.info.get("version") == b"89a":
|
||||
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)
|
||||
|
||||
return [
|
||||
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),
|
||||
]
|
||||
|
||||
|
||||
def _write_frame_data(fp, im_frame, offset, params):
|
||||
try:
|
||||
im_frame.encoderinfo = params
|
||||
|
||||
# local image header
|
||||
_write_local_header(fp, im_frame, offset, 0)
|
||||
|
||||
ImageFile._save(
|
||||
im_frame, fp, [("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, palette=None, info=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)
|
||||
|
||||
"""
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
|
||||
if info is None:
|
||||
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
|
||||
|
||||
|
||||
# To specify duration, add the time in milliseconds to getdata(),
|
||||
# e.g. getdata(im_frame, duration=1000)
|
||||
def getdata(im, offset=(0, 0), **params):
|
||||
"""
|
||||
Legacy Method
|
||||
|
||||
Return a list of strings representing this image.
|
||||
The first string is a local image header, the rest contains
|
||||
encoded image data.
|
||||
|
||||
: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
|
||||
|
||||
"""
|
||||
|
||||
class Collector:
|
||||
data = []
|
||||
|
||||
def write(self, data):
|
||||
self.data.append(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)
|
140
.venv/lib/python3.9/site-packages/PIL/GimpGradientFile.py
Normal file
140
.venv/lib/python3.9/site-packages/PIL/GimpGradientFile.py
Normal file
@ -0,0 +1,140 @@
|
||||
#
|
||||
# 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 math import log, pi, sin, sqrt
|
||||
|
||||
from ._binary import o8
|
||||
|
||||
EPSILON = 1e-10
|
||||
"""""" # Enable auto-doc for data member
|
||||
|
||||
|
||||
def linear(middle, pos):
|
||||
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, pos):
|
||||
return pos ** (log(0.5) / log(max(middle, EPSILON)))
|
||||
|
||||
|
||||
def sine(middle, pos):
|
||||
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
|
||||
|
||||
|
||||
def sphere_increasing(middle, pos):
|
||||
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
|
||||
|
||||
|
||||
def sphere_decreasing(middle, pos):
|
||||
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 = None
|
||||
|
||||
def getpalette(self, entries=256):
|
||||
|
||||
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):
|
||||
|
||||
if fp.readline()[:13] != b"GIMP Gradient":
|
||||
raise SyntaxError("not a GIMP gradient file")
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
raise OSError("cannot handle HSV colour space")
|
||||
|
||||
gradient.append((x0, x1, xm, rgb0, rgb1, segment))
|
||||
|
||||
self.gradient = gradient
|
56
.venv/lib/python3.9/site-packages/PIL/GimpPaletteFile.py
Normal file
56
.venv/lib/python3.9/site-packages/PIL/GimpPaletteFile.py
Normal file
@ -0,0 +1,56 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from ._binary import o8
|
||||
|
||||
|
||||
class GimpPaletteFile:
|
||||
"""File handler for GIMP's palette format."""
|
||||
|
||||
rawmode = "RGB"
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
self.palette = [o8(i) * 3 for i in range(256)]
|
||||
|
||||
if fp.readline()[:12] != b"GIMP Palette":
|
||||
raise SyntaxError("not a GIMP palette file")
|
||||
|
||||
for i in range(256):
|
||||
|
||||
s = fp.readline()
|
||||
if not s:
|
||||
break
|
||||
|
||||
# skip fields and comment lines
|
||||
if re.match(br"\w+:|#", s):
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
||||
v = tuple(map(int, s.split()[:3]))
|
||||
if len(v) != 3:
|
||||
raise ValueError("bad palette entry")
|
||||
|
||||
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
||||
|
||||
self.palette = b"".join(self.palette)
|
||||
|
||||
def getpalette(self):
|
||||
|
||||
return self.palette, self.rawmode
|
73
.venv/lib/python3.9/site-packages/PIL/GribStubImagePlugin.py
Normal file
73
.venv/lib/python3.9/site-packages/PIL/GribStubImagePlugin.py
Normal file
@ -0,0 +1,73 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific GRIB image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[0:4] == b"GRIB" and prefix[7] == 1
|
||||
|
||||
|
||||
class GribStubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "GRIB"
|
||||
format_description = "GRIB"
|
||||
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
raise SyntaxError("Not a GRIB file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
return _handler
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise OSError("GRIB save handler not installed")
|
||||
_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")
|
73
.venv/lib/python3.9/site-packages/PIL/Hdf5StubImagePlugin.py
Normal file
73
.venv/lib/python3.9/site-packages/PIL/Hdf5StubImagePlugin.py
Normal file
@ -0,0 +1,73 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
|
||||
_handler = None
|
||||
|
||||
|
||||
def register_handler(handler):
|
||||
"""
|
||||
Install application-specific HDF5 image handler.
|
||||
|
||||
:param handler: Handler object.
|
||||
"""
|
||||
global _handler
|
||||
_handler = handler
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
|
||||
|
||||
|
||||
class HDF5StubImageFile(ImageFile.StubImageFile):
|
||||
|
||||
format = "HDF5"
|
||||
format_description = "HDF5"
|
||||
|
||||
def _open(self):
|
||||
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(8)):
|
||||
raise SyntaxError("Not an HDF file")
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
if loader:
|
||||
loader.open(self)
|
||||
|
||||
def _load(self):
|
||||
return _handler
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if _handler is None or not hasattr("_handler", "save"):
|
||||
raise OSError("HDF5 save handler not installed")
|
||||
_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"])
|
386
.venv/lib/python3.9/site-packages/PIL/IcnsImagePlugin.py
Normal file
386
.venv/lib/python3.9/site-packages/PIL/IcnsImagePlugin.py
Normal file
@ -0,0 +1,386 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageFile, PngImagePlugin, features
|
||||
|
||||
enable_jpeg2k = features.check_codec("jpg_2000")
|
||||
if enable_jpeg2k:
|
||||
from PIL import Jpeg2KImagePlugin
|
||||
|
||||
MAGIC = b"icns"
|
||||
HEADERSIZE = 8
|
||||
|
||||
|
||||
def nextheader(fobj):
|
||||
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
|
||||
|
||||
|
||||
def read_32t(fobj, start_length, size):
|
||||
# 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":
|
||||
raise SyntaxError("Unknown signature, expecting 0x00000000")
|
||||
return read_32(fobj, (start + 4, length - 4), size)
|
||||
|
||||
|
||||
def read_32(fobj, start_length, size):
|
||||
"""
|
||||
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 = byte[0]
|
||||
if byte & 0x80:
|
||||
blocksize = byte - 125
|
||||
byte = fobj.read(1)
|
||||
for i in range(blocksize):
|
||||
data.append(byte)
|
||||
else:
|
||||
blocksize = byte + 1
|
||||
data.append(fobj.read(blocksize))
|
||||
bytesleft -= blocksize
|
||||
if bytesleft <= 0:
|
||||
break
|
||||
if bytesleft != 0:
|
||||
raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
|
||||
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, start_length, size):
|
||||
# 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, start_length, size):
|
||||
(start, length) = start_length
|
||||
fobj.seek(start)
|
||||
sig = fobj.read(12)
|
||||
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
|
||||
fobj.seek(start)
|
||||
im = PngImagePlugin.PngImageFile(fobj)
|
||||
Image._decompression_bomb_check(im.size)
|
||||
return {"RGBA": im}
|
||||
elif (
|
||||
sig[:4] == b"\xff\x4f\xff\x51"
|
||||
or sig[:4] == b"\x0d\x0a\x87\x0a"
|
||||
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
||||
):
|
||||
if not enable_jpeg2k:
|
||||
raise ValueError(
|
||||
"Unsupported icon subimage format (rebuild PIL "
|
||||
"with JPEG 2000 support to fix this)"
|
||||
)
|
||||
# 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:
|
||||
raise ValueError("Unsupported icon subimage format")
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
fobj is a file-like object as an icns resource
|
||||
"""
|
||||
# signature : (start, length)
|
||||
self.dct = dct = {}
|
||||
self.fobj = fobj
|
||||
sig, filesize = nextheader(fobj)
|
||||
if sig != MAGIC:
|
||||
raise SyntaxError("not an icns file")
|
||||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
sig, blocksize = nextheader(fobj)
|
||||
if blocksize <= 0:
|
||||
raise SyntaxError("invalid block header")
|
||||
i += HEADERSIZE
|
||||
blocksize -= HEADERSIZE
|
||||
dct[sig] = (i, blocksize)
|
||||
fobj.seek(blocksize, io.SEEK_CUR)
|
||||
i += blocksize
|
||||
|
||||
def itersizes(self):
|
||||
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):
|
||||
sizes = self.itersizes()
|
||||
if not sizes:
|
||||
raise SyntaxError("No 32bit icon resources found")
|
||||
return max(sizes)
|
||||
|
||||
def dataforsize(self, size):
|
||||
"""
|
||||
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=None):
|
||||
if size is None:
|
||||
size = self.bestsize()
|
||||
if len(size) == 2:
|
||||
size = (size[0], size[1], 1)
|
||||
channels = self.dataforsize(size)
|
||||
|
||||
im = channels.get("RGBA", None)
|
||||
if im:
|
||||
return im
|
||||
|
||||
im = channels.get("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):
|
||||
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):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
info_size = value
|
||||
if info_size not in self.info["sizes"] and len(info_size) == 2:
|
||||
info_size = (info_size[0], info_size[1], 1)
|
||||
if (
|
||||
info_size not in self.info["sizes"]
|
||||
and len(info_size) == 3
|
||||
and info_size[2] == 1
|
||||
):
|
||||
simple_sizes = [
|
||||
(size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"]
|
||||
]
|
||||
if value in simple_sizes:
|
||||
info_size = self.info["sizes"][simple_sizes.index(value)]
|
||||
if info_size not in self.info["sizes"]:
|
||||
raise ValueError("This is not one of the allowed sizes of this image")
|
||||
self._size = value
|
||||
|
||||
def load(self):
|
||||
if len(self.size) == 3:
|
||||
self.best_size = self.size
|
||||
self.size = (
|
||||
self.best_size[0] * self.best_size[2],
|
||||
self.best_size[1] * self.best_size[2],
|
||||
)
|
||||
|
||||
Image.Image.load(self)
|
||||
if self.im and self.im.size == self.size:
|
||||
# Already loaded
|
||||
return
|
||||
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
|
||||
im.load()
|
||||
|
||||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
self.size = im.size
|
||||
self.load_end()
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
"""
|
||||
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": type, "size": len(stream), "stream": stream})
|
||||
|
||||
# Header
|
||||
fp.write(MAGIC)
|
||||
fp.write(struct.pack(">i", sum(entry["size"] for entry in entries)))
|
||||
|
||||
# TOC
|
||||
fp.write(b"TOC ")
|
||||
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
|
||||
for entry in entries:
|
||||
fp.write(entry["type"])
|
||||
fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
|
||||
|
||||
# Data
|
||||
for entry in entries:
|
||||
fp.write(entry["type"])
|
||||
fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
|
||||
fp.write(entry["stream"])
|
||||
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == 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"]:
|
||||
imf.size = size
|
||||
imf.save("out-%s-%s-%s.png" % size)
|
||||
with Image.open(sys.argv[1]) as im:
|
||||
im.save("out.png")
|
||||
if sys.platform == "windows":
|
||||
os.startfile("out.png")
|
339
.venv/lib/python3.9/site-packages/PIL/IcoImagePlugin.py
Normal file
339
.venv/lib/python3.9/site-packages/PIL/IcoImagePlugin.py
Normal file
@ -0,0 +1,339 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Icon format references:
|
||||
# * https://en.wikipedia.org/wiki/ICO_(file_format)
|
||||
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
|
||||
|
||||
import struct
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from math import ceil, log
|
||||
|
||||
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
from ._binary import o32le as o32
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
_MAGIC = b"\0\0\1\0"
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
fp.write(_MAGIC) # (2+2)
|
||||
sizes = im.encoderinfo.get(
|
||||
"sizes",
|
||||
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
width, height = im.size
|
||||
sizes = filter(
|
||||
lambda x: False
|
||||
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256)
|
||||
else True,
|
||||
sizes,
|
||||
)
|
||||
sizes = list(sizes)
|
||||
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
|
||||
offset = fp.tell() + len(sizes) * 16
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
|
||||
for size in sizes:
|
||||
width, height = size
|
||||
# 0 means 256
|
||||
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
|
||||
fp.write(b"\0") # bColorCount(1)
|
||||
fp.write(b"\0") # bReserved(1)
|
||||
fp.write(b"\0\0") # wPlanes(2)
|
||||
|
||||
tmp = provided_images.get(size)
|
||||
if not tmp:
|
||||
# TODO: invent a more convenient method for proportional scalings
|
||||
tmp = im.copy()
|
||||
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
|
||||
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
|
||||
fp.write(struct.pack("<H", bits)) # wBitCount(2)
|
||||
|
||||
image_io = BytesIO()
|
||||
if bmp:
|
||||
tmp.save(image_io, "dib")
|
||||
|
||||
if bits != 32:
|
||||
and_mask = Image.new("1", tmp.size)
|
||||
ImageFile._save(
|
||||
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
|
||||
)
|
||||
else:
|
||||
tmp.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(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
|
||||
current = fp.tell()
|
||||
fp.seek(offset)
|
||||
fp.write(image_bytes)
|
||||
offset = offset + bytes_len
|
||||
fp.seek(current)
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == _MAGIC
|
||||
|
||||
|
||||
class IcoFile:
|
||||
def __init__(self, buf):
|
||||
"""
|
||||
Parse image from file-like object containing ico file data
|
||||
"""
|
||||
|
||||
# check magic
|
||||
s = buf.read(6)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not an ICO file")
|
||||
|
||||
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)
|
||||
|
||||
icon_header = {
|
||||
"width": s[0],
|
||||
"height": s[1],
|
||||
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
|
||||
"reserved": s[3],
|
||||
"planes": i16(s, 4),
|
||||
"bpp": i16(s, 6),
|
||||
"size": i32(s, 8),
|
||||
"offset": i32(s, 12),
|
||||
}
|
||||
|
||||
# See Wikipedia
|
||||
for j in ("width", "height"):
|
||||
if not icon_header[j]:
|
||||
icon_header[j] = 256
|
||||
|
||||
# See Wikipedia notes about color depth.
|
||||
# We need this just to differ images with equal sizes
|
||||
icon_header["color_depth"] = (
|
||||
icon_header["bpp"]
|
||||
or (
|
||||
icon_header["nb_color"] != 0
|
||||
and ceil(log(icon_header["nb_color"], 2))
|
||||
)
|
||||
or 256
|
||||
)
|
||||
|
||||
icon_header["dim"] = (icon_header["width"], icon_header["height"])
|
||||
icon_header["square"] = icon_header["width"] * icon_header["height"]
|
||||
|
||||
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['width'])
|
||||
self.entry = sorted(self.entry, key=lambda x: x["square"])
|
||||
self.entry.reverse()
|
||||
|
||||
def sizes(self):
|
||||
"""
|
||||
Get a list of all available icon sizes and color depths.
|
||||
"""
|
||||
return {(h["width"], h["height"]) for h in self.entry}
|
||||
|
||||
def getentryindex(self, size, bpp=False):
|
||||
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, bpp=False):
|
||||
"""
|
||||
Get an image from the icon
|
||||
"""
|
||||
return self.frame(self.getentryindex(size, bpp))
|
||||
|
||||
def frame(self, idx):
|
||||
"""
|
||||
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"])
|
||||
|
||||
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] = d, (0, 0) + im.size, o, a
|
||||
|
||||
# figure out where AND mask image starts
|
||||
bpp = header["bpp"]
|
||||
if 32 == bpp:
|
||||
# 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
|
||||
mask = Image.frombuffer(
|
||||
"L", # 8bpp
|
||||
im.size, # (w, h)
|
||||
alpha_bytes, # source chars
|
||||
"raw", # raw decoder
|
||||
("L", 0, -1), # 8bpp inverted, unpadded, reversed
|
||||
)
|
||||
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
|
||||
|
||||
and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0))
|
||||
total_bytes = int((w * im.size[1]) / 8)
|
||||
|
||||
self.buf.seek(and_mask_offset)
|
||||
mask_data = self.buf.read(total_bytes)
|
||||
|
||||
# convert raw data to image
|
||||
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
|
||||
)
|
||||
|
||||
# now we have two images, im is XOR image and mask is AND image
|
||||
|
||||
# apply mask image as alpha channel
|
||||
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):
|
||||
self.ico = IcoFile(self.fp)
|
||||
self.info["sizes"] = self.ico.sizes()
|
||||
self.size = self.ico.entry[0]["dim"]
|
||||
self.load()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, value):
|
||||
if value not in self.info["sizes"]:
|
||||
raise ValueError("This is not one of the allowed sizes of this image")
|
||||
self._size = value
|
||||
|
||||
def load(self):
|
||||
if self.im and self.im.size == self.size:
|
||||
# Already loaded
|
||||
return
|
||||
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.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
|
||||
|
||||
def load_seek(self):
|
||||
# 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")
|
376
.venv/lib/python3.9/site-packages/PIL/ImImagePlugin.py
Normal file
376
.venv/lib/python3.9/site-packages/PIL/ImImagePlugin.py
Normal file
@ -0,0 +1,376 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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": ("RGBX", "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 i in range(2, 33):
|
||||
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Read IM directory
|
||||
|
||||
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
|
||||
|
||||
def number(s):
|
||||
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):
|
||||
|
||||
# Quick rejection: if there's not an LF among the first
|
||||
# 100 bytes, this is (probably) not a text header.
|
||||
|
||||
if b"\n" not in self.fp.read(100):
|
||||
raise SyntaxError("not an IM file")
|
||||
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:
|
||||
raise SyntaxError("not an IM file")
|
||||
|
||||
if s[-2:] == b"\r\n":
|
||||
s = s[:-2]
|
||||
elif s[-1:] == b"\n":
|
||||
s = s[:-1]
|
||||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as e:
|
||||
raise SyntaxError("not an IM file") 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:
|
||||
|
||||
raise SyntaxError(
|
||||
"Syntax error in IM header: " + s.decode("ascii", "replace")
|
||||
)
|
||||
|
||||
if not n:
|
||||
raise SyntaxError("Not an IM file")
|
||||
|
||||
# Basic attributes
|
||||
self._size = self.info[SIZE]
|
||||
self.mode = self.info[MODE]
|
||||
|
||||
# Skip forward to start of image data
|
||||
while s and s[0:1] != b"\x1A":
|
||||
s = self.fp.read(1)
|
||||
if not s:
|
||||
raise SyntaxError("File truncated")
|
||||
|
||||
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[:2] == "F;":
|
||||
|
||||
# ifunc95 formats
|
||||
try:
|
||||
# use bit decoder (if necessary)
|
||||
bits = int(self.rawmode[2:])
|
||||
if bits not in [8, 16, 32]:
|
||||
self.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 = [
|
||||
("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
|
||||
("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
|
||||
("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
|
||||
]
|
||||
else:
|
||||
# LabEye/IFUNC files
|
||||
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
return self.info[FRAMES]
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
return self.info[FRAMES] > 1
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
|
||||
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 = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
|
||||
|
||||
def tell(self):
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# 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, fp, filename):
|
||||
|
||||
try:
|
||||
image_type, rawmode = SAVE[im.mode]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Cannot save {im.mode} images as IM") 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
|
||||
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(("Image size (x*y): %d*%d\r\n" % im.size).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"]:
|
||||
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
|
||||
ImageFile._save(im, fp, [("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")
|
3604
.venv/lib/python3.9/site-packages/PIL/Image.py
Normal file
3604
.venv/lib/python3.9/site-packages/PIL/Image.py
Normal file
File diff suppressed because it is too large
Load Diff
328
.venv/lib/python3.9/site-packages/PIL/ImageChops.py
Normal file
328
.venv/lib/python3.9/site-packages/PIL/ImageChops.py
Normal file
@ -0,0 +1,328 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
|
||||
|
||||
def constant(image, value):
|
||||
"""Fill a channel with a given grey level.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
|
||||
return Image.new("L", image.size, value)
|
||||
|
||||
|
||||
def duplicate(image):
|
||||
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
|
||||
return image.copy()
|
||||
|
||||
|
||||
def invert(image):
|
||||
"""
|
||||
Invert an image (channel).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
out = MAX - image
|
||||
|
||||
:rtype: :py:class:`~PIL.Image.Image`
|
||||
"""
|
||||
|
||||
image.load()
|
||||
return image._new(image.im.chop_invert())
|
||||
|
||||
|
||||
def lighter(image1, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the lighter values.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""
|
||||
Compares the two images, pixel by pixel, and returns a new image containing
|
||||
the darker values.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""
|
||||
Returns the absolute value of the pixel-by-pixel difference between the two
|
||||
images.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""
|
||||
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.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""
|
||||
Superimposes two inverted images on top of each other.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""
|
||||
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, image2):
|
||||
"""
|
||||
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, image2):
|
||||
"""
|
||||
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, image2, scale=1.0, offset=0):
|
||||
"""
|
||||
Adds two images, dividing the result by scale and adding the
|
||||
offset. If omitted, scale defaults to 1.0, and offset to 0.0.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2, scale=1.0, offset=0):
|
||||
"""
|
||||
Subtracts two images, dividing the result by scale and adding the offset.
|
||||
If omitted, scale defaults to 1.0, and offset to 0.0.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""Add two images, without clipping the result.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""Subtract two images, without clipping the result.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""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.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""Logical OR between two images.
|
||||
|
||||
Both of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2):
|
||||
"""Logical XOR between two images.
|
||||
|
||||
Both of the images must have mode "1".
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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, image2, alpha):
|
||||
"""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, image2, mask):
|
||||
"""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, xoffset, yoffset=None):
|
||||
"""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 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))
|
999
.venv/lib/python3.9/site-packages/PIL/ImageCms.py
Normal file
999
.venv/lib/python3.9/site-packages/PIL/ImageCms.py
Normal file
@ -0,0 +1,999 @@
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
|
||||
# Optional color management support, based on Kevin Cazabon's PyCMS
|
||||
# library.
|
||||
|
||||
# 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.
|
||||
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from PIL import _imagingcms
|
||||
except ImportError as ex:
|
||||
# Allow error import for doc purposes, but error out when accessing
|
||||
# anything in core.
|
||||
from ._util import deferred_error
|
||||
|
||||
_imagingcms = deferred_error(ex)
|
||||
|
||||
DESCRIPTION = """
|
||||
pyCMS
|
||||
|
||||
a Python / PIL interface to the littleCMS ICC Color Management System
|
||||
Copyright (C) 2002-2003 Kevin Cazabon
|
||||
kevin@cazabon.com
|
||||
http://www.cazabon.com
|
||||
|
||||
pyCMS home page: http://www.cazabon.com/pyCMS
|
||||
littleCMS home page: http://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"
|
||||
|
||||
# --------------------------------------------------------------------.
|
||||
|
||||
core = _imagingcms
|
||||
|
||||
#
|
||||
# intent/direction values
|
||||
|
||||
INTENT_PERCEPTUAL = 0
|
||||
INTENT_RELATIVE_COLORIMETRIC = 1
|
||||
INTENT_SATURATION = 2
|
||||
INTENT_ABSOLUTE_COLORIMETRIC = 3
|
||||
|
||||
DIRECTION_INPUT = 0
|
||||
DIRECTION_OUTPUT = 1
|
||||
DIRECTION_PROOF = 2
|
||||
|
||||
#
|
||||
# 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
|
||||
}
|
||||
|
||||
_MAX_FLAG = 0
|
||||
for flag in FLAGS.values():
|
||||
if isinstance(flag, int):
|
||||
_MAX_FLAG = _MAX_FLAG | flag
|
||||
|
||||
|
||||
# --------------------------------------------------------------------.
|
||||
# Experimental PIL-level API
|
||||
# --------------------------------------------------------------------.
|
||||
|
||||
##
|
||||
# Profile.
|
||||
|
||||
|
||||
class ImageCmsProfile:
|
||||
def __init__(self, profile):
|
||||
"""
|
||||
:param profile: Either a string representing a filename,
|
||||
a file like object containing a profile or a
|
||||
low-level profile object
|
||||
|
||||
"""
|
||||
|
||||
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._set(core.profile_frombytes(f.read()))
|
||||
return
|
||||
self._set(core.profile_open(profile), profile)
|
||||
elif hasattr(profile, "read"):
|
||||
self._set(core.profile_frombytes(profile.read()))
|
||||
elif isinstance(profile, _imagingcms.CmsProfile):
|
||||
self._set(profile)
|
||||
else:
|
||||
raise TypeError("Invalid type for Profile")
|
||||
|
||||
def _set(self, profile, filename=None):
|
||||
self.profile = profile
|
||||
self.filename = filename
|
||||
if profile:
|
||||
self.product_name = None # profile.product_name
|
||||
self.product_info = None # profile.product_info
|
||||
else:
|
||||
self.product_name = None
|
||||
self.product_info = None
|
||||
|
||||
def tobytes(self):
|
||||
"""
|
||||
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,
|
||||
output,
|
||||
input_mode,
|
||||
output_mode,
|
||||
intent=INTENT_PERCEPTUAL,
|
||||
proof=None,
|
||||
proof_intent=INTENT_ABSOLUTE_COLORIMETRIC,
|
||||
flags=0,
|
||||
):
|
||||
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):
|
||||
return self.apply(im)
|
||||
|
||||
def apply(self, im, imOut=None):
|
||||
im.load()
|
||||
if imOut is None:
|
||||
imOut = Image.new(self.output_mode, im.size, None)
|
||||
self.transform.apply(im.im.id, imOut.im.id)
|
||||
imOut.info["icc_profile"] = self.output_profile.tobytes()
|
||||
return imOut
|
||||
|
||||
def apply_in_place(self, im):
|
||||
im.load()
|
||||
if im.mode != self.output_mode:
|
||||
raise ValueError("mode mismatch") # wrong output mode
|
||||
self.transform.apply(im.im.id, im.im.id)
|
||||
im.info["icc_profile"] = self.output_profile.tobytes()
|
||||
return im
|
||||
|
||||
|
||||
def get_display_profile(handle=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 PIL import ImageWin
|
||||
|
||||
if isinstance(handle, ImageWin.HDC):
|
||||
profile = core.get_display_profile_win32(handle, 1)
|
||||
else:
|
||||
profile = core.get_display_profile_win32(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,
|
||||
inputProfile,
|
||||
outputProfile,
|
||||
renderingIntent=INTENT_PERCEPTUAL,
|
||||
outputMode=None,
|
||||
inPlace=False,
|
||||
flags=0,
|
||||
):
|
||||
"""
|
||||
(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):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
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):
|
||||
"""
|
||||
(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,
|
||||
outputProfile,
|
||||
inMode,
|
||||
outMode,
|
||||
renderingIntent=INTENT_PERCEPTUAL,
|
||||
flags=0,
|
||||
):
|
||||
"""
|
||||
(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):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
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,
|
||||
outputProfile,
|
||||
proofProfile,
|
||||
inMode,
|
||||
outMode,
|
||||
renderingIntent=INTENT_PERCEPTUAL,
|
||||
proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC,
|
||||
flags=FLAGS["SOFTPROOFING"],
|
||||
):
|
||||
"""
|
||||
(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):
|
||||
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
|
||||
|
||||
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
|
||||
raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
|
||||
|
||||
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, transform, inPlace=False):
|
||||
"""
|
||||
(pyCMS) Applies a transform to a given image.
|
||||
|
||||
If ``im.mode != transform.inMode``, a :exc:`PyCMSError` is raised.
|
||||
|
||||
If ``inPlace`` is ``True`` and ``transform.inMode != transform.outMode``, a
|
||||
:exc:`PyCMSError` is raised.
|
||||
|
||||
If ``im.mode``, ``transform.inMode`` or ``transform.outMode`` 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.inMode`` and ``transform.outMode`` 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.outMode``.
|
||||
|
||||
:param im: An :py:class:`~PIL.Image.Image` object, and im.mode must be the same
|
||||
as the ``inMode`` 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, colorTemp=-1):
|
||||
"""
|
||||
(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 integer 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"]:
|
||||
raise PyCMSError(
|
||||
f"Color space not supported for on-the-fly profile creation ({colorSpace})"
|
||||
)
|
||||
|
||||
if colorSpace == "LAB":
|
||||
try:
|
||||
colorTemp = float(colorTemp)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise PyCMSError(
|
||||
f'Color temperature must be numeric, "{colorTemp}" not valid'
|
||||
) from e
|
||||
|
||||
try:
|
||||
return core.createProfile(colorSpace, colorTemp)
|
||||
except (TypeError, ValueError) as v:
|
||||
raise PyCMSError(v) from v
|
||||
|
||||
|
||||
def getProfileName(profile):
|
||||
"""
|
||||
|
||||
(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 len(model) > 30:
|
||||
return model + "\n"
|
||||
return f"{model} - {manufacturer}\n"
|
||||
|
||||
except (AttributeError, OSError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v) from v
|
||||
|
||||
|
||||
def getProfileInfo(profile):
|
||||
"""
|
||||
(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
|
||||
arr = []
|
||||
for elt in (description, cpright):
|
||||
if elt:
|
||||
arr.append(elt)
|
||||
return "\r\n\r\n".join(arr) + "\r\n\r\n"
|
||||
|
||||
except (AttributeError, OSError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v) from v
|
||||
|
||||
|
||||
def getProfileCopyright(profile):
|
||||
"""
|
||||
(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):
|
||||
"""
|
||||
(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):
|
||||
"""
|
||||
(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):
|
||||
"""
|
||||
(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):
|
||||
"""
|
||||
(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, intent, direction):
|
||||
"""
|
||||
(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
|
||||
|
||||
|
||||
def versions():
|
||||
"""
|
||||
(pyCMS) Fetches versions.
|
||||
"""
|
||||
|
||||
return (VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__)
|
300
.venv/lib/python3.9/site-packages/PIL/ImageColor.py
Normal file
300
.venv/lib/python3.9/site-packages/PIL/ImageColor.py
Normal file
@ -0,0 +1,300 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from . import Image
|
||||
|
||||
|
||||
def getrgb(color):
|
||||
"""
|
||||
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])``
|
||||
"""
|
||||
color = color.lower()
|
||||
|
||||
rgb = colormap.get(color, None)
|
||||
if rgb:
|
||||
if isinstance(rgb, tuple):
|
||||
return rgb
|
||||
colormap[color] = rgb = getrgb(rgb)
|
||||
return rgb
|
||||
|
||||
# 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 = hls_to_rgb(
|
||||
float(m.group(1)) / 360.0,
|
||||
float(m.group(3)) / 100.0,
|
||||
float(m.group(2)) / 100.0,
|
||||
)
|
||||
return (
|
||||
int(rgb[0] * 255 + 0.5),
|
||||
int(rgb[1] * 255 + 0.5),
|
||||
int(rgb[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 = hsv_to_rgb(
|
||||
float(m.group(1)) / 360.0,
|
||||
float(m.group(2)) / 100.0,
|
||||
float(m.group(3)) / 100.0,
|
||||
)
|
||||
return (
|
||||
int(rgb[0] * 255 + 0.5),
|
||||
int(rgb[1] * 255 + 0.5),
|
||||
int(rgb[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)))
|
||||
raise ValueError(f"unknown color specifier: {repr(color)}")
|
||||
|
||||
|
||||
def getcolor(color, mode):
|
||||
"""
|
||||
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
|
||||
greyscale value if the mode is not color or a palette image. If the string
|
||||
cannot be parsed, this function raises a :py:exc:`ValueError` exception.
|
||||
|
||||
.. versionadded:: 1.1.4
|
||||
|
||||
:param color: A color string
|
||||
:return: ``(graylevel [, alpha]) or (red, green, blue[, alpha])``
|
||||
"""
|
||||
# same as getrgb, but converts the result to the given mode
|
||||
color, alpha = getrgb(color), 255
|
||||
if len(color) == 4:
|
||||
color, alpha = color[0:3], color[3]
|
||||
|
||||
if Image.getmodebase(mode) == "L":
|
||||
r, g, b = color
|
||||
# ITU-R Recommendation 601-2 for nonlinear RGB
|
||||
# scaled to 24 bits to match the convert's implementation.
|
||||
color = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
|
||||
if mode[-1] == "A":
|
||||
return (color, alpha)
|
||||
else:
|
||||
if mode[-1] == "A":
|
||||
return color + (alpha,)
|
||||
return color
|
||||
|
||||
|
||||
colormap = {
|
||||
# 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",
|
||||
}
|
984
.venv/lib/python3.9/site-packages/PIL/ImageDraw.py
Normal file
984
.venv/lib/python3.9/site-packages/PIL/ImageDraw.py
Normal file
@ -0,0 +1,984 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import math
|
||||
import numbers
|
||||
|
||||
from . import Image, ImageColor, ImageFont
|
||||
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
<p>
|
||||
Application code should use the <b>Draw</b> factory, instead of
|
||||
directly.
|
||||
"""
|
||||
|
||||
|
||||
class ImageDraw:
|
||||
def __init__(self, im, mode=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.load()
|
||||
if im.readonly:
|
||||
im._copy() # make it writeable
|
||||
blend = 0
|
||||
if mode is None:
|
||||
mode = im.mode
|
||||
if mode != im.mode:
|
||||
if mode == "RGBA" and im.mode == "RGB":
|
||||
blend = 1
|
||||
else:
|
||||
raise ValueError("mode mismatch")
|
||||
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 = 0
|
||||
self.font = None
|
||||
|
||||
def getfont(self):
|
||||
"""
|
||||
Get the current default font.
|
||||
|
||||
: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 _getink(self, ink, fill=None):
|
||||
if ink is None and fill is None:
|
||||
if self.fill:
|
||||
fill = self.ink
|
||||
else:
|
||||
ink = self.ink
|
||||
else:
|
||||
if ink is not None:
|
||||
if isinstance(ink, str):
|
||||
ink = ImageColor.getcolor(ink, self.mode)
|
||||
if self.palette and not isinstance(ink, numbers.Number):
|
||||
ink = self.palette.getcolor(ink, self._image)
|
||||
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 not isinstance(fill, numbers.Number):
|
||||
fill = self.palette.getcolor(fill, self._image)
|
||||
fill = self.draw.draw_ink(fill)
|
||||
return ink, fill
|
||||
|
||||
def arc(self, xy, start, end, fill=None, width=1):
|
||||
"""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, bitmap, fill=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, start, end, fill=None, outline=None, width=1):
|
||||
"""Draw a chord."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_chord(xy, start, end, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
self.draw.draw_chord(xy, start, end, ink, 0, width)
|
||||
|
||||
def ellipse(self, xy, fill=None, outline=None, width=1):
|
||||
"""Draw an ellipse."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_ellipse(xy, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
self.draw.draw_ellipse(xy, ink, 0, width)
|
||||
|
||||
def line(self, xy, fill=None, width=0, joint=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:
|
||||
if not isinstance(xy[0], (list, tuple)):
|
||||
xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)]
|
||||
for i in range(1, len(xy) - 1):
|
||||
point = xy[i]
|
||||
angles = [
|
||||
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
|
||||
% 360
|
||||
for start, end in ((xy[i - 1], point), (point, xy[i + 1]))
|
||||
]
|
||||
if angles[0] == angles[1]:
|
||||
# This is a straight line, so no joint is required
|
||||
continue
|
||||
|
||||
def coord_at_angle(coord, angle):
|
||||
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:
|
||||
gapCoords = [
|
||||
coord_at_angle(point, angles[0] + 90),
|
||||
point,
|
||||
coord_at_angle(point, angles[1] + 90),
|
||||
]
|
||||
else:
|
||||
gapCoords = [
|
||||
coord_at_angle(point, angles[0] - 90),
|
||||
point,
|
||||
coord_at_angle(point, angles[1] - 90),
|
||||
]
|
||||
self.line(gapCoords, fill, width=3)
|
||||
|
||||
def shape(self, shape, fill=None, outline=None):
|
||||
"""(Experimental) Draw a shape."""
|
||||
shape.close()
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_outline(shape, fill, 1)
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_outline(shape, ink, 0)
|
||||
|
||||
def pieslice(self, xy, start, end, fill=None, outline=None, width=1):
|
||||
"""Draw a pieslice."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_pieslice(xy, start, end, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
|
||||
|
||||
def point(self, xy, fill=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, fill=None, outline=None):
|
||||
"""Draw a polygon."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_polygon(xy, fill, 1)
|
||||
if ink is not None and ink != fill:
|
||||
self.draw.draw_polygon(xy, ink, 0)
|
||||
|
||||
def regular_polygon(
|
||||
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None
|
||||
):
|
||||
"""Draw a regular polygon."""
|
||||
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
||||
self.polygon(xy, fill, outline)
|
||||
|
||||
def rectangle(self, xy, fill=None, outline=None, width=1):
|
||||
"""Draw a rectangle."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
self.draw.draw_rectangle(xy, fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
self.draw.draw_rectangle(xy, ink, 0, width)
|
||||
|
||||
def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1):
|
||||
"""Draw a rounded rectangle."""
|
||||
if isinstance(xy[0], (list, tuple)):
|
||||
(x0, y0), (x1, y1) = xy
|
||||
else:
|
||||
x0, y0, x1, y1 = xy
|
||||
|
||||
d = radius * 2
|
||||
|
||||
full_x = d >= x1 - x0
|
||||
if full_x:
|
||||
# The two left and two right corners are joined
|
||||
d = x1 - x0
|
||||
full_y = d >= y1 - y0
|
||||
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:
|
||||
# If the corners have no curve, that is a rectangle
|
||||
return self.rectangle(xy, fill, outline, width)
|
||||
|
||||
r = d // 2
|
||||
ink, fill = self._getink(outline, fill)
|
||||
|
||||
def draw_corners(pieslice):
|
||||
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 = (
|
||||
((x1 - d, y0, x1, y0 + d), 270, 360),
|
||||
((x1 - d, y1 - d, x1, y1), 0, 90),
|
||||
((x0, y1 - d, x0 + d, y1), 90, 180),
|
||||
((x0, y0, x0 + d, y0 + d), 180, 270),
|
||||
)
|
||||
for part in parts:
|
||||
if pieslice:
|
||||
self.draw.draw_pieslice(*(part + (fill, 1)))
|
||||
else:
|
||||
self.draw.draw_arc(*(part + (ink, width)))
|
||||
|
||||
if fill is not None:
|
||||
draw_corners(True)
|
||||
|
||||
if full_x:
|
||||
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
|
||||
else:
|
||||
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
|
||||
if not full_x and not full_y:
|
||||
self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1)
|
||||
self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1)
|
||||
if ink is not None and ink != fill and width != 0:
|
||||
draw_corners(False)
|
||||
|
||||
if not full_x:
|
||||
self.draw.draw_rectangle(
|
||||
(x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1
|
||||
)
|
||||
self.draw.draw_rectangle(
|
||||
(x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1
|
||||
)
|
||||
if not full_y:
|
||||
self.draw.draw_rectangle(
|
||||
(x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1
|
||||
)
|
||||
self.draw.draw_rectangle(
|
||||
(x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1
|
||||
)
|
||||
|
||||
def _multiline_check(self, text):
|
||||
"""Draw text."""
|
||||
split_character = "\n" if isinstance(text, str) else b"\n"
|
||||
|
||||
return split_character in text
|
||||
|
||||
def _multiline_split(self, text):
|
||||
split_character = "\n" if isinstance(text, str) else b"\n"
|
||||
|
||||
return text.split(split_character)
|
||||
|
||||
def text(
|
||||
self,
|
||||
xy,
|
||||
text,
|
||||
fill=None,
|
||||
font=None,
|
||||
anchor=None,
|
||||
spacing=4,
|
||||
align="left",
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
stroke_fill=None,
|
||||
embedded_color=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_text(
|
||||
xy,
|
||||
text,
|
||||
fill,
|
||||
font,
|
||||
anchor,
|
||||
spacing,
|
||||
align,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
stroke_fill,
|
||||
embedded_color,
|
||||
)
|
||||
|
||||
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
||||
raise ValueError("Embedded color supported only in RGB and RGBA modes")
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
|
||||
def getink(fill):
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is None:
|
||||
return fill
|
||||
return ink
|
||||
|
||||
def draw_text(ink, stroke_width=0, stroke_offset=None):
|
||||
mode = self.fontmode
|
||||
if stroke_width == 0 and embedded_color:
|
||||
mode = "RGBA"
|
||||
coord = xy
|
||||
try:
|
||||
mask, offset = font.getmask2(
|
||||
text,
|
||||
mode,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
anchor=anchor,
|
||||
ink=ink,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
coord = coord[0] + offset[0], coord[1] + offset[1]
|
||||
except AttributeError:
|
||||
try:
|
||||
mask = font.getmask(
|
||||
text,
|
||||
mode,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
anchor,
|
||||
ink,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
except TypeError:
|
||||
mask = font.getmask(text)
|
||||
if stroke_offset:
|
||||
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
|
||||
if mode == "RGBA":
|
||||
# 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)
|
||||
color.fillband(3, (ink >> 24) & 0xFF)
|
||||
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
|
||||
self.im.paste(color, coord + coord2, mask)
|
||||
else:
|
||||
self.draw.draw_bitmap(coord, mask, ink)
|
||||
|
||||
ink = getink(fill)
|
||||
if ink is not None:
|
||||
stroke_ink = None
|
||||
if stroke_width:
|
||||
stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
|
||||
|
||||
if stroke_ink is not None:
|
||||
# Draw stroked text
|
||||
draw_text(stroke_ink, stroke_width)
|
||||
|
||||
# Draw normal text
|
||||
draw_text(ink, 0)
|
||||
else:
|
||||
# Only draw normal text
|
||||
draw_text(ink)
|
||||
|
||||
def multiline_text(
|
||||
self,
|
||||
xy,
|
||||
text,
|
||||
fill=None,
|
||||
font=None,
|
||||
anchor=None,
|
||||
spacing=4,
|
||||
align="left",
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
stroke_fill=None,
|
||||
embedded_color=False,
|
||||
):
|
||||
if direction == "ttb":
|
||||
raise ValueError("ttb direction is unsupported for multiline text")
|
||||
|
||||
if anchor is None:
|
||||
anchor = "la"
|
||||
elif len(anchor) != 2:
|
||||
raise ValueError("anchor must be a 2 character string")
|
||||
elif anchor[1] in "tb":
|
||||
raise ValueError("anchor not supported for multiline text")
|
||||
|
||||
widths = []
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = (
|
||||
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
|
||||
)
|
||||
for line in lines:
|
||||
line_width = self.textlength(
|
||||
line, font, direction=direction, features=features, language=language
|
||||
)
|
||||
widths.append(line_width)
|
||||
max_width = max(max_width, line_width)
|
||||
|
||||
top = xy[1]
|
||||
if anchor[1] == "m":
|
||||
top -= (len(lines) - 1) * line_spacing / 2.0
|
||||
elif anchor[1] == "d":
|
||||
top -= (len(lines) - 1) * line_spacing
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
left = xy[0]
|
||||
width_difference = max_width - widths[idx]
|
||||
|
||||
# first align left by anchor
|
||||
if anchor[0] == "m":
|
||||
left -= width_difference / 2.0
|
||||
elif anchor[0] == "r":
|
||||
left -= width_difference
|
||||
|
||||
# then align by align parameter
|
||||
if align == "left":
|
||||
pass
|
||||
elif align == "center":
|
||||
left += width_difference / 2.0
|
||||
elif align == "right":
|
||||
left += width_difference
|
||||
else:
|
||||
raise ValueError('align must be "left", "center" or "right"')
|
||||
|
||||
self.text(
|
||||
(left, top),
|
||||
line,
|
||||
fill,
|
||||
font,
|
||||
anchor,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
stroke_fill=stroke_fill,
|
||||
embedded_color=embedded_color,
|
||||
)
|
||||
top += line_spacing
|
||||
|
||||
def textsize(
|
||||
self,
|
||||
text,
|
||||
font=None,
|
||||
spacing=4,
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
):
|
||||
"""Get the size of a given string, in pixels."""
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_textsize(
|
||||
text, font, spacing, direction, features, language, stroke_width
|
||||
)
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
return font.getsize(text, direction, features, language, stroke_width)
|
||||
|
||||
def multiline_textsize(
|
||||
self,
|
||||
text,
|
||||
font=None,
|
||||
spacing=4,
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
):
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = (
|
||||
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
|
||||
)
|
||||
for line in lines:
|
||||
line_width, line_height = self.textsize(
|
||||
line, font, spacing, direction, features, language, stroke_width
|
||||
)
|
||||
max_width = max(max_width, line_width)
|
||||
return max_width, len(lines) * line_spacing - spacing
|
||||
|
||||
def textlength(
|
||||
self,
|
||||
text,
|
||||
font=None,
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
embedded_color=False,
|
||||
):
|
||||
"""Get the length of a given string, in pixels with 1/64 precision."""
|
||||
if self._multiline_check(text):
|
||||
raise ValueError("can't measure length of multiline text")
|
||||
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
||||
raise ValueError("Embedded color supported only in RGB and RGBA modes")
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
mode = "RGBA" if embedded_color else self.fontmode
|
||||
try:
|
||||
return font.getlength(text, mode, direction, features, language)
|
||||
except AttributeError:
|
||||
size = self.textsize(
|
||||
text, font, direction=direction, features=features, language=language
|
||||
)
|
||||
if direction == "ttb":
|
||||
return size[1]
|
||||
return size[0]
|
||||
|
||||
def textbbox(
|
||||
self,
|
||||
xy,
|
||||
text,
|
||||
font=None,
|
||||
anchor=None,
|
||||
spacing=4,
|
||||
align="left",
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
embedded_color=False,
|
||||
):
|
||||
"""Get the bounding box of a given string, in pixels."""
|
||||
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
||||
raise ValueError("Embedded color supported only in RGB and RGBA modes")
|
||||
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_textbbox(
|
||||
xy,
|
||||
text,
|
||||
font,
|
||||
anchor,
|
||||
spacing,
|
||||
align,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
embedded_color,
|
||||
)
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
if not isinstance(font, ImageFont.FreeTypeFont):
|
||||
raise ValueError("Only supported for TrueType fonts")
|
||||
mode = "RGBA" if embedded_color else self.fontmode
|
||||
bbox = font.getbbox(
|
||||
text, mode, direction, features, language, stroke_width, anchor
|
||||
)
|
||||
return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1]
|
||||
|
||||
def multiline_textbbox(
|
||||
self,
|
||||
xy,
|
||||
text,
|
||||
font=None,
|
||||
anchor=None,
|
||||
spacing=4,
|
||||
align="left",
|
||||
direction=None,
|
||||
features=None,
|
||||
language=None,
|
||||
stroke_width=0,
|
||||
embedded_color=False,
|
||||
):
|
||||
if direction == "ttb":
|
||||
raise ValueError("ttb direction is unsupported for multiline text")
|
||||
|
||||
if anchor is None:
|
||||
anchor = "la"
|
||||
elif len(anchor) != 2:
|
||||
raise ValueError("anchor must be a 2 character string")
|
||||
elif anchor[1] in "tb":
|
||||
raise ValueError("anchor not supported for multiline text")
|
||||
|
||||
widths = []
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = (
|
||||
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
|
||||
)
|
||||
for line in lines:
|
||||
line_width = self.textlength(
|
||||
line,
|
||||
font,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language,
|
||||
embedded_color=embedded_color,
|
||||
)
|
||||
widths.append(line_width)
|
||||
max_width = max(max_width, line_width)
|
||||
|
||||
top = xy[1]
|
||||
if anchor[1] == "m":
|
||||
top -= (len(lines) - 1) * line_spacing / 2.0
|
||||
elif anchor[1] == "d":
|
||||
top -= (len(lines) - 1) * line_spacing
|
||||
|
||||
bbox = None
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
left = xy[0]
|
||||
width_difference = max_width - widths[idx]
|
||||
|
||||
# first align left by anchor
|
||||
if anchor[0] == "m":
|
||||
left -= width_difference / 2.0
|
||||
elif anchor[0] == "r":
|
||||
left -= width_difference
|
||||
|
||||
# then align by align parameter
|
||||
if align == "left":
|
||||
pass
|
||||
elif align == "center":
|
||||
left += width_difference / 2.0
|
||||
elif align == "right":
|
||||
left += width_difference
|
||||
else:
|
||||
raise ValueError('align must be "left", "center" or "right"')
|
||||
|
||||
bbox_line = self.textbbox(
|
||||
(left, top),
|
||||
line,
|
||||
font,
|
||||
anchor,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
embedded_color=embedded_color,
|
||||
)
|
||||
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]),
|
||||
)
|
||||
|
||||
top += line_spacing
|
||||
|
||||
if bbox is None:
|
||||
return xy[0], xy[1], xy[0], xy[1]
|
||||
return bbox
|
||||
|
||||
|
||||
def Draw(im, mode=None):
|
||||
"""
|
||||
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 im.getdraw(mode)
|
||||
except AttributeError:
|
||||
return ImageDraw(im, mode)
|
||||
|
||||
|
||||
# experimental access to the outline API
|
||||
try:
|
||||
Outline = Image.core.outline
|
||||
except AttributeError:
|
||||
Outline = None
|
||||
|
||||
|
||||
def getdraw(im=None, hints=None):
|
||||
"""
|
||||
(Experimental) A more advanced 2D drawing interface for PIL images,
|
||||
based on the WCK interface.
|
||||
|
||||
:param im: The image to draw in.
|
||||
:param hints: An optional list of hints.
|
||||
:returns: A (drawing context, drawing resource factory) tuple.
|
||||
"""
|
||||
# FIXME: this needs more work!
|
||||
# FIXME: come up with a better 'hints' scheme.
|
||||
handler = None
|
||||
if not hints or "nicest" in hints:
|
||||
try:
|
||||
from . import _imagingagg as handler
|
||||
except ImportError:
|
||||
pass
|
||||
if handler is None:
|
||||
from . import ImageDraw2 as handler
|
||||
if im:
|
||||
im = handler.Draw(im)
|
||||
return im, handler
|
||||
|
||||
|
||||
def floodfill(image, xy, value, border=None, thresh=0):
|
||||
"""
|
||||
(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()
|
||||
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 != value and p != 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, n_sides, rotation):
|
||||
"""
|
||||
Generate a list of vertices for a 2D regular polygon.
|
||||
|
||||
:param bounding_circle: The bounding circle is a tuple 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):
|
||||
raise TypeError("n_sides should be an int")
|
||||
if n_sides < 3:
|
||||
raise ValueError("n_sides should be an int > 2")
|
||||
|
||||
# 1.2 Check `bounding_circle` has an appropriate value
|
||||
if not isinstance(bounding_circle, (list, tuple)):
|
||||
raise TypeError("bounding_circle should be a tuple")
|
||||
|
||||
if len(bounding_circle) == 3:
|
||||
*centroid, polygon_radius = bounding_circle
|
||||
elif len(bounding_circle) == 2:
|
||||
centroid, polygon_radius = bounding_circle
|
||||
else:
|
||||
raise ValueError(
|
||||
"bounding_circle should contain 2D coordinates "
|
||||
"and a radius (e.g. (x, y, r) or ((x, y), r) )"
|
||||
)
|
||||
|
||||
if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
|
||||
raise ValueError("bounding_circle should only contain numeric data")
|
||||
|
||||
if not len(centroid) == 2:
|
||||
raise ValueError(
|
||||
"bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
|
||||
)
|
||||
|
||||
if polygon_radius <= 0:
|
||||
raise ValueError("bounding_circle radius should be > 0")
|
||||
|
||||
# 1.3 Check `rotation` has an appropriate value
|
||||
if not isinstance(rotation, (int, float)):
|
||||
raise ValueError("rotation should be an int or float")
|
||||
|
||||
# 2. Define Helper Functions
|
||||
def _apply_rotation(point, degrees, centroid):
|
||||
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(centroid, polygon_radius, angle):
|
||||
start_point = [polygon_radius, 0]
|
||||
return _apply_rotation(start_point, angle, centroid)
|
||||
|
||||
def _get_angles(n_sides, rotation):
|
||||
angles = []
|
||||
degrees = 360 / n_sides
|
||||
# Start with the bottom left polygon vertex
|
||||
current_angle = (270 - 0.5 * degrees) + rotation
|
||||
for _ in range(0, 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(centroid, polygon_radius, angle) for angle in angles
|
||||
]
|
||||
|
||||
|
||||
def _color_diff(color1, color2):
|
||||
"""
|
||||
Uses 1-norm distance to calculate difference between two values.
|
||||
"""
|
||||
if isinstance(color2, tuple):
|
||||
return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))])
|
||||
else:
|
||||
return abs(color1 - color2)
|
179
.venv/lib/python3.9/site-packages/PIL/ImageDraw2.py
Normal file
179
.venv/lib/python3.9/site-packages/PIL/ImageDraw2.py
Normal file
@ -0,0 +1,179 @@
|
||||
#
|
||||
# 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 . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
|
||||
|
||||
|
||||
class Pen:
|
||||
"""Stores an outline color and width."""
|
||||
|
||||
def __init__(self, color, width=1, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
self.width = width
|
||||
|
||||
|
||||
class Brush:
|
||||
"""Stores a fill color"""
|
||||
|
||||
def __init__(self, color, opacity=255):
|
||||
self.color = ImageColor.getrgb(color)
|
||||
|
||||
|
||||
class Font:
|
||||
"""Stores a TrueType font and color"""
|
||||
|
||||
def __init__(self, color, file, size=12):
|
||||
# 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, size=None, color=None):
|
||||
if not hasattr(image, "im"):
|
||||
image = Image.new(image, size, color)
|
||||
self.draw = ImageDraw.Draw(image)
|
||||
self.image = image
|
||||
self.transform = None
|
||||
|
||||
def flush(self):
|
||||
return self.image
|
||||
|
||||
def render(self, op, xy, pen, brush=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:
|
||||
xy = ImagePath.Path(xy)
|
||||
xy.transform(self.transform)
|
||||
# render the item
|
||||
if op == "line":
|
||||
self.draw.line(xy, fill=outline, width=width)
|
||||
else:
|
||||
getattr(self.draw, op)(xy, fill=fill, outline=outline)
|
||||
|
||||
def settransform(self, offset):
|
||||
"""Sets a transformation offset."""
|
||||
(xoffset, yoffset) = offset
|
||||
self.transform = (1, 0, xoffset, 0, 1, yoffset)
|
||||
|
||||
def arc(self, xy, start, end, *options):
|
||||
"""
|
||||
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, start, end, *options)
|
||||
|
||||
def chord(self, xy, start, end, *options):
|
||||
"""
|
||||
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, start, end, *options)
|
||||
|
||||
def ellipse(self, xy, *options):
|
||||
"""
|
||||
Draws an ellipse inside the given bounding box.
|
||||
|
||||
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
|
||||
"""
|
||||
self.render("ellipse", xy, *options)
|
||||
|
||||
def line(self, xy, *options):
|
||||
"""
|
||||
Draws a line between the coordinates in the ``xy`` list.
|
||||
|
||||
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
|
||||
"""
|
||||
self.render("line", xy, *options)
|
||||
|
||||
def pieslice(self, xy, start, end, *options):
|
||||
"""
|
||||
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, start, end, *options)
|
||||
|
||||
def polygon(self, xy, *options):
|
||||
"""
|
||||
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, *options)
|
||||
|
||||
def rectangle(self, xy, *options):
|
||||
"""
|
||||
Draws a rectangle.
|
||||
|
||||
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
|
||||
"""
|
||||
self.render("rectangle", xy, *options)
|
||||
|
||||
def text(self, xy, text, font):
|
||||
"""
|
||||
Draws the string at the given position.
|
||||
|
||||
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text`
|
||||
"""
|
||||
if self.transform:
|
||||
xy = ImagePath.Path(xy)
|
||||
xy.transform(self.transform)
|
||||
self.draw.text(xy, text, font=font.font, fill=font.color)
|
||||
|
||||
def textsize(self, text, font):
|
||||
"""
|
||||
Return the size of the given string, in pixels.
|
||||
|
||||
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize`
|
||||
"""
|
||||
return self.draw.textsize(text, font=font.font)
|
103
.venv/lib/python3.9/site-packages/PIL/ImageEnhance.py
Normal file
103
.venv/lib/python3.9/site-packages/PIL/ImageEnhance.py
Normal file
@ -0,0 +1,103 @@
|
||||
#
|
||||
# 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 . import Image, ImageFilter, ImageStat
|
||||
|
||||
|
||||
class _Enhance:
|
||||
def enhance(self, factor):
|
||||
"""
|
||||
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):
|
||||
self.image = image
|
||||
self.intermediate_mode = "L"
|
||||
if "A" in image.getbands():
|
||||
self.intermediate_mode = "LA"
|
||||
|
||||
self.degenerate = image.convert(self.intermediate_mode).convert(image.mode)
|
||||
|
||||
|
||||
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 grey image. A factor of 1.0 gives the original image.
|
||||
"""
|
||||
|
||||
def __init__(self, image):
|
||||
self.image = image
|
||||
mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5)
|
||||
self.degenerate = Image.new("L", image.size, mean).convert(image.mode)
|
||||
|
||||
if "A" in image.getbands():
|
||||
self.degenerate.putalpha(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):
|
||||
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):
|
||||
self.image = image
|
||||
self.degenerate = image.filter(ImageFilter.SMOOTH)
|
||||
|
||||
if "A" in image.getbands():
|
||||
self.degenerate.putalpha(image.getchannel("A"))
|
699
.venv/lib/python3.9/site-packages/PIL/ImageFile.py
Normal file
699
.venv/lib/python3.9/site-packages/PIL/ImageFile.py
Normal file
@ -0,0 +1,699 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import io
|
||||
import struct
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from . import Image
|
||||
from ._util import isPath
|
||||
|
||||
MAXBLOCK = 65536
|
||||
|
||||
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`."""
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
|
||||
def raise_oserror(error):
|
||||
try:
|
||||
message = Image.core.getcodecstatus(error)
|
||||
except AttributeError:
|
||||
message = ERRORS.get(error)
|
||||
if not message:
|
||||
message = f"decoder error {error}"
|
||||
raise OSError(message + " when reading image file")
|
||||
|
||||
|
||||
def raise_ioerror(error):
|
||||
warnings.warn(
|
||||
"raise_ioerror is deprecated and will be removed in Pillow 9 (2022-01-02). "
|
||||
"Use raise_oserror instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return raise_oserror(error)
|
||||
|
||||
|
||||
def _tilesort(t):
|
||||
# sort on offset
|
||||
return t[2]
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# ImageFile base class
|
||||
|
||||
|
||||
class ImageFile(Image.Image):
|
||||
"""Base class for image file format handlers."""
|
||||
|
||||
def __init__(self, fp=None, filename=None):
|
||||
super().__init__()
|
||||
|
||||
self._min_frame = 0
|
||||
|
||||
self.custom_mimetype = None
|
||||
|
||||
self.tile = None
|
||||
""" A list of tile descriptors, or ``None`` """
|
||||
|
||||
self.readonly = 1 # until we know better
|
||||
|
||||
self.decoderconfig = ()
|
||||
self.decodermaxblock = MAXBLOCK
|
||||
|
||||
if isPath(fp):
|
||||
# filename
|
||||
self.fp = open(fp, "rb")
|
||||
self.filename = fp
|
||||
self._exclusive_fp = True
|
||||
else:
|
||||
# stream
|
||||
self.fp = fp
|
||||
self.filename = filename
|
||||
# can be overridden
|
||||
self._exclusive_fp = None
|
||||
|
||||
try:
|
||||
try:
|
||||
self._open()
|
||||
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:
|
||||
raise SyntaxError("not identified by this driver")
|
||||
except BaseException:
|
||||
# close the file only if we have opened it this constructor
|
||||
if self._exclusive_fp:
|
||||
self.fp.close()
|
||||
raise
|
||||
|
||||
def get_format_mimetype(self):
|
||||
if self.custom_mimetype:
|
||||
return self.custom_mimetype
|
||||
if self.format is not None:
|
||||
return Image.MIME.get(self.format.upper())
|
||||
|
||||
def verify(self):
|
||||
"""Check file integrity"""
|
||||
|
||||
# raise exception if something's wrong. must be called
|
||||
# directly after open, and closes file when finished.
|
||||
if self._exclusive_fp:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
|
||||
def load(self):
|
||||
"""Load image data based on tile list"""
|
||||
|
||||
if self.tile is None:
|
||||
raise OSError("cannot load this image")
|
||||
|
||||
pixel = Image.Image.load(self)
|
||||
if not self.tile:
|
||||
return pixel
|
||||
|
||||
self.map = None
|
||||
use_mmap = self.filename and len(self.tile) == 1
|
||||
# As of pypy 2.1.0, memory mapping was failing here.
|
||||
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
|
||||
|
||||
readonly = 0
|
||||
|
||||
# look for read/seek overrides
|
||||
try:
|
||||
read = self.load_read
|
||||
# don't use mmap if there are custom read/seek functions
|
||||
use_mmap = False
|
||||
except AttributeError:
|
||||
read = self.fp.read
|
||||
|
||||
try:
|
||||
seek = self.load_seek
|
||||
use_mmap = False
|
||||
except AttributeError:
|
||||
seek = self.fp.seek
|
||||
|
||||
if use_mmap:
|
||||
# try memory mapping
|
||||
decoder_name, extents, offset, args = self.tile[0]
|
||||
if (
|
||||
decoder_name == "raw"
|
||||
and len(args) >= 3
|
||||
and args[0] == self.mode
|
||||
and args[0] in Image._MAPMODES
|
||||
):
|
||||
try:
|
||||
# use mmap, if possible
|
||||
import mmap
|
||||
|
||||
with open(self.filename) as fp:
|
||||
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
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)
|
||||
|
||||
try:
|
||||
# FIXME: This is a hack to handle TIFF's JpegTables tag.
|
||||
prefix = self.tile_prefix
|
||||
except AttributeError:
|
||||
prefix = b""
|
||||
|
||||
for decoder_name, extents, offset, args in self.tile:
|
||||
decoder = Image._getdecoder(
|
||||
self.mode, decoder_name, args, self.decoderconfig
|
||||
)
|
||||
try:
|
||||
seek(offset)
|
||||
decoder.setimage(self.im, extents)
|
||||
if decoder.pulls_fd:
|
||||
decoder.setfd(self.fp)
|
||||
status, err_code = decoder.decode(b"")
|
||||
else:
|
||||
b = prefix
|
||||
while True:
|
||||
try:
|
||||
s = read(self.decodermaxblock)
|
||||
except (IndexError, struct.error) as e:
|
||||
# truncated png/gif
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise OSError("image file is truncated") from e
|
||||
|
||||
if not s: # truncated jpeg
|
||||
if LOAD_TRUNCATED_IMAGES:
|
||||
break
|
||||
else:
|
||||
raise OSError(
|
||||
"image file is truncated "
|
||||
f"({len(b)} bytes not processed)"
|
||||
)
|
||||
|
||||
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_oserror(err_code)
|
||||
|
||||
return Image.Image.load(self)
|
||||
|
||||
def load_prepare(self):
|
||||
# create image memory if necessary
|
||||
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
|
||||
self.im = Image.core.new(self.mode, self.size)
|
||||
# create palette (optional)
|
||||
if self.mode == "P":
|
||||
Image.Image.load(self)
|
||||
|
||||
def load_end(self):
|
||||
# may be overridden
|
||||
pass
|
||||
|
||||
# may be defined for contained formats
|
||||
# def load_seek(self, pos):
|
||||
# pass
|
||||
|
||||
# may be defined for blocked formats (e.g. PNG)
|
||||
# def load_read(self, bytes):
|
||||
# pass
|
||||
|
||||
def _seek_check(self, frame):
|
||||
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 >= self.n_frames + self._min_frame
|
||||
)
|
||||
):
|
||||
raise EOFError("attempt to seek outside sequence")
|
||||
|
||||
return self.tell() != frame
|
||||
|
||||
|
||||
class StubImageFile(ImageFile):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
def _open(self):
|
||||
raise NotImplementedError("StubImageFile subclass must implement _open")
|
||||
|
||||
def load(self):
|
||||
loader = self._load()
|
||||
if loader is None:
|
||||
raise OSError(f"cannot find loader for this {self.format} file")
|
||||
image = loader.load(self)
|
||||
assert image is not None
|
||||
# become the other object (!)
|
||||
self.__class__ = image.__class__
|
||||
self.__dict__ = image.__dict__
|
||||
|
||||
def _load(self):
|
||||
"""(Hook) Find actual image loader."""
|
||||
raise NotImplementedError("StubImageFile subclass must implement _load")
|
||||
|
||||
|
||||
class Parser:
|
||||
"""
|
||||
Incremental image parser. This class implements the standard
|
||||
feed/close consumer interface.
|
||||
"""
|
||||
|
||||
incremental = None
|
||||
image = None
|
||||
data = None
|
||||
decoder = None
|
||||
offset = 0
|
||||
finished = 0
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
(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):
|
||||
"""
|
||||
(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_oserror(e)
|
||||
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:
|
||||
# traceback.print_exc()
|
||||
pass # not enough data
|
||||
else:
|
||||
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
||||
if flag or len(im.tile) != 1:
|
||||
# custom load code, or multiple tiles
|
||||
self.decode = None
|
||||
else:
|
||||
# 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):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
(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:
|
||||
raise OSError("image was incomplete")
|
||||
if not self.image:
|
||||
raise OSError("cannot parse this image")
|
||||
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, fp, tile, bufsize=0):
|
||||
"""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:
|
||||
stdout = fp == sys.stdout or fp == sys.stdout.buffer
|
||||
except (OSError, AttributeError):
|
||||
stdout = False
|
||||
if stdout:
|
||||
fp.flush()
|
||||
return
|
||||
try:
|
||||
fh = fp.fileno()
|
||||
fp.flush()
|
||||
except (AttributeError, io.UnsupportedOperation) as exc:
|
||||
# compress to Python file-compatible object
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if s < 0:
|
||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||
e.cleanup()
|
||||
else:
|
||||
# slight speedup: compress to real file object
|
||||
for e, b, o, a in tile:
|
||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
if o > 0:
|
||||
fp.seek(o)
|
||||
e.setimage(im.im, b)
|
||||
if e.pushes_fd:
|
||||
e.setfd(fp)
|
||||
l, s = e.encode_to_pyfd()
|
||||
else:
|
||||
s = e.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise OSError(f"encoder error {s} when writing image file")
|
||||
e.cleanup()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
||||
def _safe_read(fp, size):
|
||||
"""
|
||||
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:
|
||||
raise OSError("Truncated File Read")
|
||||
return data
|
||||
data = []
|
||||
while size > 0:
|
||||
block = fp.read(min(size, SAFEBLOCK))
|
||||
if not block:
|
||||
break
|
||||
data.append(block)
|
||||
size -= len(block)
|
||||
if sum(len(d) for d in data) < size:
|
||||
raise OSError("Truncated File Read")
|
||||
return b"".join(data)
|
||||
|
||||
|
||||
class PyCodecState:
|
||||
def __init__(self):
|
||||
self.xsize = 0
|
||||
self.ysize = 0
|
||||
self.xoff = 0
|
||||
self.yoff = 0
|
||||
|
||||
def extents(self):
|
||||
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)
|
||||
|
||||
|
||||
class PyDecoder:
|
||||
"""
|
||||
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 Decoder in Python<file-decoders-py>`
|
||||
"""
|
||||
|
||||
_pulls_fd = False
|
||||
|
||||
def __init__(self, mode, *args):
|
||||
self.im = None
|
||||
self.state = PyCodecState()
|
||||
self.fd = None
|
||||
self.mode = mode
|
||||
self.init(args)
|
||||
|
||||
def init(self, args):
|
||||
"""
|
||||
Override to perform decoder specific initialization
|
||||
|
||||
:param args: Array of args items from the tile entry
|
||||
:returns: None
|
||||
"""
|
||||
self.args = args
|
||||
|
||||
@property
|
||||
def pulls_fd(self):
|
||||
return self._pulls_fd
|
||||
|
||||
def decode(self, buffer):
|
||||
"""
|
||||
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 <0 for the bytes consumed.
|
||||
Err codes are from :data:`.ImageFile.ERRORS`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Override to perform decoder specific cleanup
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
pass
|
||||
|
||||
def setfd(self, fd):
|
||||
"""
|
||||
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, extents=None):
|
||||
"""
|
||||
Called from ImageFile to set the core output image for the decoder
|
||||
|
||||
: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
|
||||
else:
|
||||
(x0, y0, x1, y1) = (0, 0, 0, 0)
|
||||
|
||||
if x0 == 0 and x1 == 0:
|
||||
self.state.xsize, self.state.ysize = self.im.size
|
||||
else:
|
||||
self.state.xoff = x0
|
||||
self.state.yoff = y0
|
||||
self.state.xsize = x1 - x0
|
||||
self.state.ysize = y1 - y0
|
||||
|
||||
if self.state.xsize <= 0 or self.state.ysize <= 0:
|
||||
raise ValueError("Size cannot be negative")
|
||||
|
||||
if (
|
||||
self.state.xsize + self.state.xoff > self.im.size[0]
|
||||
or self.state.ysize + self.state.yoff > self.im.size[1]
|
||||
):
|
||||
raise ValueError("Tile cannot extend outside image")
|
||||
|
||||
def set_as_raw(self, data, rawmode=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
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
if not rawmode:
|
||||
rawmode = self.mode
|
||||
d = Image._getdecoder(self.mode, "raw", (rawmode))
|
||||
d.setimage(self.im, self.state.extents())
|
||||
s = d.decode(data)
|
||||
|
||||
if s[0] >= 0:
|
||||
raise ValueError("not enough image data")
|
||||
if s[1] != 0:
|
||||
raise ValueError("cannot decode image data")
|
538
.venv/lib/python3.9/site-packages/PIL/ImageFilter.py
Normal file
538
.venv/lib/python3.9/site-packages/PIL/ImageFilter.py
Normal file
@ -0,0 +1,538 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import functools
|
||||
|
||||
|
||||
class Filter:
|
||||
pass
|
||||
|
||||
|
||||
class MultibandFilter(Filter):
|
||||
pass
|
||||
|
||||
|
||||
class BuiltinFilter(MultibandFilter):
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
return image.filter(*self.filterargs)
|
||||
|
||||
|
||||
class Kernel(BuiltinFilter):
|
||||
"""
|
||||
Create a convolution kernel. The current version only
|
||||
supports 3x3 and 5x5 integer and floating point kernels.
|
||||
|
||||
In the current version, kernels can only be applied to
|
||||
"L" and "RGB" images.
|
||||
|
||||
:param size: Kernel size, given as (width, height). In the current
|
||||
version, this must be (3,3) or (5,5).
|
||||
:param kernel: A sequence containing kernel weights.
|
||||
: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, kernel, scale=None, offset=0):
|
||||
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):
|
||||
raise ValueError("not enough coefficients in kernel")
|
||||
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, rank):
|
||||
self.size = size
|
||||
self.rank = rank
|
||||
|
||||
def filter(self, image):
|
||||
if image.mode == "P":
|
||||
raise ValueError("cannot filter palette images")
|
||||
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=3):
|
||||
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=3):
|
||||
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=3):
|
||||
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=3):
|
||||
self.size = size
|
||||
|
||||
def filter(self, image):
|
||||
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.
|
||||
"""
|
||||
|
||||
name = "GaussianBlur"
|
||||
|
||||
def __init__(self, radius=2):
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.gaussian_blur(self.radius)
|
||||
|
||||
|
||||
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 one direction. 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):
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.box_blur(self.radius)
|
||||
|
||||
|
||||
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
|
||||
|
||||
""" # noqa: E501
|
||||
|
||||
name = "UnsharpMask"
|
||||
|
||||
def __init__(self, radius=2, percent=150, threshold=3):
|
||||
self.radius = radius
|
||||
self.percent = percent
|
||||
self.threshold = threshold
|
||||
|
||||
def filter(self, image):
|
||||
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, table, channels=3, target_mode=None, **kwargs):
|
||||
if channels not in (3, 4):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
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 = None
|
||||
if hasattr(table, "shape"):
|
||||
try:
|
||||
import numpy
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
if numpy and isinstance(table, numpy.ndarray):
|
||||
if copy_table:
|
||||
table = table.copy()
|
||||
|
||||
if table.shape in [
|
||||
(items * channels,),
|
||||
(items, channels),
|
||||
(size[2], size[1], size[0], channels),
|
||||
]:
|
||||
table = 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)):
|
||||
table, raw_table = [], table
|
||||
for pixel in raw_table:
|
||||
if len(pixel) != channels:
|
||||
raise ValueError(
|
||||
"The elements of the table should "
|
||||
"have a length of {}.".format(channels)
|
||||
)
|
||||
table.extend(pixel)
|
||||
|
||||
if wrong_size or len(table) != items * channels:
|
||||
raise ValueError(
|
||||
"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)}"
|
||||
)
|
||||
self.table = table
|
||||
|
||||
@staticmethod
|
||||
def _check_size(size):
|
||||
try:
|
||||
_, _, _ = size
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"Size should be either an integer or a tuple of three integers."
|
||||
) from e
|
||||
except TypeError:
|
||||
size = (size, size, size)
|
||||
size = [int(x) for x in size]
|
||||
for size1D in size:
|
||||
if not 2 <= size1D <= 65:
|
||||
raise ValueError("Size should be in [2, 65] range.")
|
||||
return size
|
||||
|
||||
@classmethod
|
||||
def generate(cls, size, callback, channels=3, target_mode=None):
|
||||
"""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.
|
||||
"""
|
||||
size1D, size2D, size3D = cls._check_size(size)
|
||||
if channels not in (3, 4):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
|
||||
table = [0] * (size1D * size2D * size3D * channels)
|
||||
idx_out = 0
|
||||
for b in range(size3D):
|
||||
for g in range(size2D):
|
||||
for r in range(size1D):
|
||||
table[idx_out : idx_out + channels] = callback(
|
||||
r / (size1D - 1), g / (size2D - 1), b / (size3D - 1)
|
||||
)
|
||||
idx_out += channels
|
||||
|
||||
return cls(
|
||||
(size1D, size2D, size3D),
|
||||
table,
|
||||
channels=channels,
|
||||
target_mode=target_mode,
|
||||
_copy_table=False,
|
||||
)
|
||||
|
||||
def transform(self, callback, with_normals=False, channels=None, target_mode=None):
|
||||
"""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):
|
||||
raise ValueError("Only 3 or 4 output channels are supported")
|
||||
ch_in = self.channels
|
||||
ch_out = channels or ch_in
|
||||
size1D, size2D, size3D = self.size
|
||||
|
||||
table = [0] * (size1D * size2D * size3D * ch_out)
|
||||
idx_in = 0
|
||||
idx_out = 0
|
||||
for b in range(size3D):
|
||||
for g in range(size2D):
|
||||
for r in range(size1D):
|
||||
values = self.table[idx_in : idx_in + ch_in]
|
||||
if with_normals:
|
||||
values = callback(
|
||||
r / (size1D - 1),
|
||||
g / (size2D - 1),
|
||||
b / (size3D - 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):
|
||||
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):
|
||||
from . import Image
|
||||
|
||||
return image.color_lut_3d(
|
||||
self.mode or image.mode,
|
||||
Image.LINEAR,
|
||||
self.channels,
|
||||
self.size[0],
|
||||
self.size[1],
|
||||
self.size[2],
|
||||
self.table,
|
||||
)
|
1060
.venv/lib/python3.9/site-packages/PIL/ImageFont.py
Normal file
1060
.venv/lib/python3.9/site-packages/PIL/ImageFont.py
Normal file
File diff suppressed because it is too large
Load Diff
120
.venv/lib/python3.9/site-packages/PIL/ImageGrab.py
Normal file
120
.venv/lib/python3.9/site-packages/PIL/ImageGrab.py
Normal file
@ -0,0 +1,120 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
from . import Image
|
||||
|
||||
if sys.platform == "darwin":
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
|
||||
if xdisplay is None:
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
os.close(fh)
|
||||
subprocess.call(["screencapture", "-x", filepath])
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
if bbox:
|
||||
im_cropped = im.crop(bbox)
|
||||
im.close()
|
||||
return im_cropped
|
||||
return im
|
||||
elif sys.platform == "win32":
|
||||
offset, size, data = Image.core.grabscreen_win32(
|
||||
include_layered_windows, all_screens
|
||||
)
|
||||
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
|
||||
# use xdisplay=None for default display on non-win32/macOS systems
|
||||
if not Image.core.HAVE_XCB:
|
||||
raise OSError("Pillow was built without XCB support")
|
||||
size, data = Image.core.grabscreen_x11(xdisplay)
|
||||
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
|
||||
if bbox:
|
||||
im = im.crop(bbox)
|
||||
return im
|
||||
|
||||
|
||||
def grabclipboard():
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".jpg")
|
||||
os.close(fh)
|
||||
commands = [
|
||||
'set theFile to (open for access POSIX file "'
|
||||
+ filepath
|
||||
+ '" with write permission)',
|
||||
"try",
|
||||
" write (the clipboard as JPEG picture) to theFile",
|
||||
"end try",
|
||||
"close access theFile",
|
||||
]
|
||||
script = ["osascript"]
|
||||
for command in commands:
|
||||
script += ["-e", command]
|
||||
subprocess.call(script)
|
||||
|
||||
im = None
|
||||
if os.stat(filepath).st_size != 0:
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
return im
|
||||
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("utf-16le").split("\0")
|
||||
else:
|
||||
files = data[o:].decode("mbcs").split("\0")
|
||||
return files[: files.index("")]
|
||||
if isinstance(data, bytes):
|
||||
import io
|
||||
|
||||
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:
|
||||
raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only")
|
253
.venv/lib/python3.9/site-packages/PIL/ImageMath.py
Normal file
253
.venv/lib/python3.9/site-packages/PIL/ImageMath.py
Normal file
@ -0,0 +1,253 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import builtins
|
||||
|
||||
from . import Image, _imagingmath
|
||||
|
||||
VERBOSE = 0
|
||||
|
||||
|
||||
def _isconstant(v):
|
||||
return isinstance(v, (int, float))
|
||||
|
||||
|
||||
class _Operand:
|
||||
"""Wraps an image operand, providing standard operators"""
|
||||
|
||||
def __init__(self, im):
|
||||
self.im = im
|
||||
|
||||
def __fixup(self, im1):
|
||||
# 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:
|
||||
raise ValueError(f"unsupported mode: {im1.im.mode}")
|
||||
else:
|
||||
# argument was a constant
|
||||
if _isconstant(im1) 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, im1, im2=None, mode=None):
|
||||
im1 = self.__fixup(im1)
|
||||
if im2 is None:
|
||||
# unary operation
|
||||
out = Image.new(mode or im1.mode, im1.size, None)
|
||||
im1.load()
|
||||
try:
|
||||
op = getattr(_imagingmath, op + "_" + im1.mode)
|
||||
except AttributeError as e:
|
||||
raise TypeError(f"bad operand type for '{op}'") from e
|
||||
_imagingmath.unop(op, out.im.id, im1.im.id)
|
||||
else:
|
||||
# binary operation
|
||||
im2 = self.__fixup(im2)
|
||||
if im1.mode != im2.mode:
|
||||
# convert both arguments to floating point
|
||||
if im1.mode != "F":
|
||||
im1 = im1.convert("F")
|
||||
if im2.mode != "F":
|
||||
im2 = im2.convert("F")
|
||||
if im1.mode != im2.mode:
|
||||
raise ValueError("mode mismatch")
|
||||
if im1.size != im2.size:
|
||||
# crop both arguments to a common size
|
||||
size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1]))
|
||||
if im1.size != size:
|
||||
im1 = im1.crop((0, 0) + size)
|
||||
if im2.size != size:
|
||||
im2 = im2.crop((0, 0) + size)
|
||||
out = Image.new(mode or im1.mode, size, None)
|
||||
else:
|
||||
out = Image.new(mode or im1.mode, im1.size, None)
|
||||
im1.load()
|
||||
im2.load()
|
||||
try:
|
||||
op = getattr(_imagingmath, op + "_" + im1.mode)
|
||||
except AttributeError as e:
|
||||
raise TypeError(f"bad operand type for '{op}'") from e
|
||||
_imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
|
||||
return _Operand(out)
|
||||
|
||||
# unary operators
|
||||
def __bool__(self):
|
||||
# an image is "true" if it contains at least one non-zero pixel
|
||||
return self.im.getbbox() is not None
|
||||
|
||||
def __abs__(self):
|
||||
return self.apply("abs", self)
|
||||
|
||||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def __neg__(self):
|
||||
return self.apply("neg", self)
|
||||
|
||||
# binary operators
|
||||
def __add__(self, other):
|
||||
return self.apply("add", self, other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.apply("add", other, self)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.apply("sub", self, other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.apply("sub", other, self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.apply("mul", self, other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.apply("mul", other, self)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.apply("div", self, other)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self.apply("div", other, self)
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.apply("mod", self, other)
|
||||
|
||||
def __rmod__(self, other):
|
||||
return self.apply("mod", other, self)
|
||||
|
||||
def __pow__(self, other):
|
||||
return self.apply("pow", self, other)
|
||||
|
||||
def __rpow__(self, other):
|
||||
return self.apply("pow", other, self)
|
||||
|
||||
# bitwise
|
||||
def __invert__(self):
|
||||
return self.apply("invert", self)
|
||||
|
||||
def __and__(self, other):
|
||||
return self.apply("and", self, other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.apply("and", other, self)
|
||||
|
||||
def __or__(self, other):
|
||||
return self.apply("or", self, other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.apply("or", other, self)
|
||||
|
||||
def __xor__(self, other):
|
||||
return self.apply("xor", self, other)
|
||||
|
||||
def __rxor__(self, other):
|
||||
return self.apply("xor", other, self)
|
||||
|
||||
def __lshift__(self, other):
|
||||
return self.apply("lshift", self, other)
|
||||
|
||||
def __rshift__(self, other):
|
||||
return self.apply("rshift", self, other)
|
||||
|
||||
# logical
|
||||
def __eq__(self, other):
|
||||
return self.apply("eq", self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.apply("ne", self, other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.apply("lt", self, other)
|
||||
|
||||
def __le__(self, other):
|
||||
return self.apply("le", self, other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.apply("gt", self, other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.apply("ge", self, other)
|
||||
|
||||
|
||||
# conversions
|
||||
def imagemath_int(self):
|
||||
return _Operand(self.im.convert("I"))
|
||||
|
||||
|
||||
def imagemath_float(self):
|
||||
return _Operand(self.im.convert("F"))
|
||||
|
||||
|
||||
# logical
|
||||
def imagemath_equal(self, other):
|
||||
return self.apply("eq", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_notequal(self, other):
|
||||
return self.apply("ne", self, other, mode="I")
|
||||
|
||||
|
||||
def imagemath_min(self, other):
|
||||
return self.apply("min", self, other)
|
||||
|
||||
|
||||
def imagemath_max(self, other):
|
||||
return self.apply("max", self, other)
|
||||
|
||||
|
||||
def imagemath_convert(self, mode):
|
||||
return _Operand(self.im.convert(mode))
|
||||
|
||||
|
||||
ops = {}
|
||||
for k, v in list(globals().items()):
|
||||
if k[:10] == "imagemath_":
|
||||
ops[k[10:]] = v
|
||||
|
||||
|
||||
def eval(expression, _dict={}, **kw):
|
||||
"""
|
||||
Evaluates an image expression.
|
||||
|
||||
:param expression: A string containing a Python-style expression.
|
||||
:param options: Values to add to the evaluation context. You
|
||||
can either use a dictionary, or one or more keyword
|
||||
arguments.
|
||||
: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 = ops.copy()
|
||||
args.update(_dict)
|
||||
args.update(kw)
|
||||
for k, v in list(args.items()):
|
||||
if hasattr(v, "im"):
|
||||
args[k] = _Operand(v)
|
||||
|
||||
out = builtins.eval(expression, args)
|
||||
try:
|
||||
return out.im
|
||||
except AttributeError:
|
||||
return out
|
74
.venv/lib/python3.9/site-packages/PIL/ImageMode.py
Normal file
74
.venv/lib/python3.9/site-packages/PIL/ImageMode.py
Normal file
@ -0,0 +1,74 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# mode descriptor cache
|
||||
_modes = None
|
||||
|
||||
|
||||
class ModeDescriptor:
|
||||
"""Wrapper for mode strings."""
|
||||
|
||||
def __init__(self, mode, bands, basemode, basetype):
|
||||
self.mode = mode
|
||||
self.bands = bands
|
||||
self.basemode = basemode
|
||||
self.basetype = basetype
|
||||
|
||||
def __str__(self):
|
||||
return self.mode
|
||||
|
||||
|
||||
def getmode(mode):
|
||||
"""Gets a mode descriptor for the given mode."""
|
||||
global _modes
|
||||
if not _modes:
|
||||
# initialize mode cache
|
||||
modes = {}
|
||||
for m, (basemode, basetype, bands) in {
|
||||
# core modes
|
||||
"1": ("L", "L", ("1",)),
|
||||
"L": ("L", "L", ("L",)),
|
||||
"I": ("L", "I", ("I",)),
|
||||
"F": ("L", "F", ("F",)),
|
||||
"P": ("P", "L", ("P",)),
|
||||
"RGB": ("RGB", "L", ("R", "G", "B")),
|
||||
"RGBX": ("RGB", "L", ("R", "G", "B", "X")),
|
||||
"RGBA": ("RGB", "L", ("R", "G", "B", "A")),
|
||||
"CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
|
||||
"YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
|
||||
"LAB": ("RGB", "L", ("L", "A", "B")),
|
||||
"HSV": ("RGB", "L", ("H", "S", "V")),
|
||||
# extra experimental modes
|
||||
"RGBa": ("RGB", "L", ("R", "G", "B", "a")),
|
||||
"LA": ("L", "L", ("L", "A")),
|
||||
"La": ("L", "L", ("L", "a")),
|
||||
"PA": ("RGB", "L", ("P", "A")),
|
||||
}.items():
|
||||
modes[m] = ModeDescriptor(m, bands, basemode, basetype)
|
||||
# mapping modes
|
||||
for i16mode in (
|
||||
"I;16",
|
||||
"I;16S",
|
||||
"I;16L",
|
||||
"I;16LS",
|
||||
"I;16B",
|
||||
"I;16BS",
|
||||
"I;16N",
|
||||
"I;16NS",
|
||||
):
|
||||
modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L")
|
||||
# set global mode cache atomically
|
||||
_modes = modes
|
||||
return _modes[mode]
|
245
.venv/lib/python3.9/site-packages/PIL/ImageMorph.py
Normal file
245
.venv/lib/python3.9/site-packages/PIL/ImageMorph.py
Normal file
@ -0,0 +1,245 @@
|
||||
# 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>
|
||||
|
||||
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=None, op_name=None):
|
||||
if patterns is not None:
|
||||
self.patterns = patterns
|
||||
else:
|
||||
self.patterns = []
|
||||
self.lut = 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:
|
||||
raise Exception("Unknown pattern " + op_name + "!")
|
||||
|
||||
self.patterns = known_patterns[op_name]
|
||||
|
||||
def add_patterns(self, patterns):
|
||||
self.patterns += patterns
|
||||
|
||||
def build_default_lut(self):
|
||||
symbols = [0, 1]
|
||||
m = 1 << 4 # pos of current pixel
|
||||
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
|
||||
|
||||
def get_lut(self):
|
||||
return self.lut
|
||||
|
||||
def _string_permute(self, pattern, permutation):
|
||||
"""string_permute 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, options, basic_result):
|
||||
"""pattern_permute 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[0:n]:
|
||||
patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res))
|
||||
|
||||
# negate
|
||||
if "N" in options:
|
||||
n = len(patterns)
|
||||
for pattern, res in patterns[0: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):
|
||||
"""Compile all patterns into a morphology lut.
|
||||
|
||||
TBD :Build based on (file) morphlut:modify_lut
|
||||
"""
|
||||
self.build_default_lut()
|
||||
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:
|
||||
raise Exception('Syntax error in pattern "' + p + '"')
|
||||
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
|
||||
for i, pattern in enumerate(patterns):
|
||||
p = pattern[0].replace(".", "X").replace("X", "[01]")
|
||||
p = re.compile(p)
|
||||
patterns[i] = (p, pattern[1])
|
||||
|
||||
# Step through table and find patterns that match.
|
||||
# Note that all the patterns are searched. The last one
|
||||
# caught overrides
|
||||
for i in range(LUT_SIZE):
|
||||
# Build the bit pattern
|
||||
bitpattern = bin(i)[2:]
|
||||
bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
|
||||
|
||||
for p, r in patterns:
|
||||
if p.match(bitpattern):
|
||||
self.lut[i] = [0, 1][r]
|
||||
|
||||
return self.lut
|
||||
|
||||
|
||||
class MorphOp:
|
||||
"""A class for binary morphological operators"""
|
||||
|
||||
def __init__(self, lut=None, op_name=None, patterns=None):
|
||||
"""Create a binary morphological operator"""
|
||||
self.lut = lut
|
||||
if op_name is not None:
|
||||
self.lut = LutBuilder(op_name=op_name).build_lut()
|
||||
elif patterns is not None:
|
||||
self.lut = LutBuilder(patterns=patterns).build_lut()
|
||||
|
||||
def apply(self, image):
|
||||
"""Run a single morphological operation on an image
|
||||
|
||||
Returns a tuple of the number of changed pixels and the
|
||||
morphed image"""
|
||||
if self.lut is None:
|
||||
raise Exception("No operator loaded")
|
||||
|
||||
if image.mode != "L":
|
||||
raise ValueError("Image mode must be L")
|
||||
outimage = Image.new(image.mode, image.size, None)
|
||||
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
|
||||
return count, outimage
|
||||
|
||||
def match(self, image):
|
||||
"""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`."""
|
||||
if self.lut is None:
|
||||
raise Exception("No operator loaded")
|
||||
|
||||
if image.mode != "L":
|
||||
raise ValueError("Image mode must be L")
|
||||
return _imagingmorph.match(bytes(self.lut), image.im.id)
|
||||
|
||||
def get_on_pixels(self, image):
|
||||
"""Get a list of all turned on pixels in a binary image
|
||||
|
||||
Returns a list of tuples of (x,y) coordinates
|
||||
of all matching pixels. See :ref:`coordinate-system`."""
|
||||
|
||||
if image.mode != "L":
|
||||
raise ValueError("Image mode must be L")
|
||||
return _imagingmorph.get_on_pixels(image.im.id)
|
||||
|
||||
def load_lut(self, filename):
|
||||
"""Load an operator from an mrl file"""
|
||||
with open(filename, "rb") as f:
|
||||
self.lut = bytearray(f.read())
|
||||
|
||||
if len(self.lut) != LUT_SIZE:
|
||||
self.lut = None
|
||||
raise Exception("Wrong size operator file!")
|
||||
|
||||
def save_lut(self, filename):
|
||||
"""Save an operator to an mrl file"""
|
||||
if self.lut is None:
|
||||
raise Exception("No operator loaded")
|
||||
with open(filename, "wb") as f:
|
||||
f.write(self.lut)
|
||||
|
||||
def set_lut(self, lut):
|
||||
"""Set the lut from an external source"""
|
||||
self.lut = lut
|
607
.venv/lib/python3.9/site-packages/PIL/ImageOps.py
Normal file
607
.venv/lib/python3.9/site-packages/PIL/ImageOps.py
Normal file
@ -0,0 +1,607 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import functools
|
||||
import operator
|
||||
import re
|
||||
|
||||
from . import Image, ImageDraw
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
|
||||
def _border(border):
|
||||
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, mode):
|
||||
if isinstance(color, str):
|
||||
from . import ImageColor
|
||||
|
||||
color = ImageColor.getcolor(color, mode)
|
||||
return color
|
||||
|
||||
|
||||
def _lut(image, lut):
|
||||
if image.mode == "P":
|
||||
# FIXME: apply to lookup table, not image data
|
||||
raise NotImplementedError("mode P support coming soon")
|
||||
elif image.mode in ("L", "RGB"):
|
||||
if image.mode == "RGB" and len(lut) == 256:
|
||||
lut = lut + lut + lut
|
||||
return image.point(lut)
|
||||
else:
|
||||
raise OSError("not supported for this image mode")
|
||||
|
||||
|
||||
#
|
||||
# actions
|
||||
|
||||
|
||||
def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
||||
"""
|
||||
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
|
||||
try:
|
||||
h[ignore] = 0
|
||||
except TypeError:
|
||||
# assume sequence
|
||||
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 = 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 = 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, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
|
||||
"""
|
||||
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
|
||||
black = _color(black, "RGB")
|
||||
white = _color(white, "RGB")
|
||||
if mid is not None:
|
||||
mid = _color(mid, "RGB")
|
||||
|
||||
# Empty lists for the mapping
|
||||
red = []
|
||||
green = []
|
||||
blue = []
|
||||
|
||||
# Create the low-end values
|
||||
for i in range(0, blackpoint):
|
||||
red.append(black[0])
|
||||
green.append(black[1])
|
||||
blue.append(black[2])
|
||||
|
||||
# Create the mapping (2-color)
|
||||
if mid is None:
|
||||
|
||||
range_map = range(0, whitepoint - blackpoint)
|
||||
|
||||
for i in range_map:
|
||||
red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
|
||||
green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
|
||||
blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
|
||||
|
||||
# Create the mapping (3-color)
|
||||
else:
|
||||
|
||||
range_map1 = range(0, midpoint - blackpoint)
|
||||
range_map2 = range(0, whitepoint - midpoint)
|
||||
|
||||
for i in range_map1:
|
||||
red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
|
||||
green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
|
||||
blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
|
||||
for i in range_map2:
|
||||
red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
|
||||
green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
|
||||
blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
|
||||
|
||||
# Create the high-end values
|
||||
for i in range(0, 256 - whitepoint):
|
||||
red.append(white[0])
|
||||
green.append(white[1])
|
||||
blue.append(white[2])
|
||||
|
||||
# Return converted image
|
||||
image = image.convert("RGB")
|
||||
return _lut(image, red + green + blue)
|
||||
|
||||
|
||||
def contain(image, size, method=Image.BICUBIC):
|
||||
"""
|
||||
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 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.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 = int(image.height / image.width * size[0])
|
||||
if new_height != size[1]:
|
||||
size = (size[0], new_height)
|
||||
else:
|
||||
new_width = int(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, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):
|
||||
"""
|
||||
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.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.width != size[0]:
|
||||
x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
|
||||
out.paste(resized, (x, 0))
|
||||
else:
|
||||
y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
|
||||
out.paste(resized, (0, y))
|
||||
return out
|
||||
|
||||
|
||||
def crop(image, border=0):
|
||||
"""
|
||||
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, factor, resample=Image.BICUBIC):
|
||||
"""
|
||||
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.BICUBIC`. See :ref:`concept-filters`.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
"""
|
||||
if factor == 1:
|
||||
return image.copy()
|
||||
elif factor <= 0:
|
||||
raise ValueError("the factor must be greater than 0")
|
||||
else:
|
||||
size = (round(factor * image.width), round(factor * image.height))
|
||||
return image.resize(size, resample)
|
||||
|
||||
|
||||
def deform(image, deformer, resample=Image.BILINEAR):
|
||||
"""
|
||||
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.MESH, deformer.getmesh(image), resample)
|
||||
|
||||
|
||||
def equalize(image, mask=None):
|
||||
"""
|
||||
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, border=0, fill=0):
|
||||
"""
|
||||
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.mode == "P" and image.palette:
|
||||
out = Image.new(image.mode, (width, height))
|
||||
out.putpalette(image.palette)
|
||||
out.paste(image, (left, top))
|
||||
|
||||
draw = ImageDraw.Draw(out)
|
||||
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
|
||||
else:
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
out.paste(image, (left, top))
|
||||
return out
|
||||
|
||||
|
||||
def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
|
||||
"""
|
||||
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.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
|
||||
# http://www.cazabon.com
|
||||
|
||||
# ensure centering is mutable
|
||||
centering = list(centering)
|
||||
|
||||
if not 0.0 <= centering[0] <= 1.0:
|
||||
centering[0] = 0.5
|
||||
if not 0.0 <= centering[1] <= 1.0:
|
||||
centering[1] = 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[0]
|
||||
crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
|
||||
|
||||
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):
|
||||
"""
|
||||
Flip the image vertically (top to bottom).
|
||||
|
||||
:param image: The image to flip.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
|
||||
def grayscale(image):
|
||||
"""
|
||||
Convert the image to grayscale.
|
||||
|
||||
:param image: The image to convert.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.convert("L")
|
||||
|
||||
|
||||
def invert(image):
|
||||
"""
|
||||
Invert (negate) the image.
|
||||
|
||||
:param image: The image to invert.
|
||||
:return: An image.
|
||||
"""
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(255 - i)
|
||||
return _lut(image, lut)
|
||||
|
||||
|
||||
def mirror(image):
|
||||
"""
|
||||
Flip image horizontally (left to right).
|
||||
|
||||
:param image: The image to mirror.
|
||||
:return: An image.
|
||||
"""
|
||||
return image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
|
||||
|
||||
def posterize(image, bits):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
lut = []
|
||||
mask = ~(2 ** (8 - bits) - 1)
|
||||
for i in range(256):
|
||||
lut.append(i & mask)
|
||||
return _lut(image, lut)
|
||||
|
||||
|
||||
def solarize(image, threshold=128):
|
||||
"""
|
||||
Invert all pixel values above a threshold.
|
||||
|
||||
:param image: The image to solarize.
|
||||
:param threshold: All pixels above this greyscale 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)
|
||||
|
||||
|
||||
def exif_transpose(image):
|
||||
"""
|
||||
If an image has an EXIF Orientation tag, return a new image that is
|
||||
transposed accordingly. Otherwise, return a copy of the image.
|
||||
|
||||
:param image: The image to transpose.
|
||||
:return: An image.
|
||||
"""
|
||||
exif = image.getexif()
|
||||
orientation = exif.get(0x0112)
|
||||
method = {
|
||||
2: Image.FLIP_LEFT_RIGHT,
|
||||
3: Image.ROTATE_180,
|
||||
4: Image.FLIP_TOP_BOTTOM,
|
||||
5: Image.TRANSPOSE,
|
||||
6: Image.ROTATE_270,
|
||||
7: Image.TRANSVERSE,
|
||||
8: Image.ROTATE_90,
|
||||
}.get(orientation)
|
||||
if method is not None:
|
||||
transposed_image = image.transpose(method)
|
||||
transposed_exif = transposed_image.getexif()
|
||||
if 0x0112 in transposed_exif:
|
||||
del transposed_exif[0x0112]
|
||||
if "exif" in transposed_image.info:
|
||||
transposed_image.info["exif"] = transposed_exif.tobytes()
|
||||
elif "Raw profile type exif" in transposed_image.info:
|
||||
transposed_image.info[
|
||||
"Raw profile type exif"
|
||||
] = transposed_exif.tobytes().hex()
|
||||
elif "XML:com.adobe.xmp" in transposed_image.info:
|
||||
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
|
||||
r'tiff:Orientation="([0-9])"',
|
||||
"",
|
||||
transposed_image.info["XML:com.adobe.xmp"],
|
||||
)
|
||||
return transposed_image
|
||||
return image.copy()
|
257
.venv/lib/python3.9/site-packages/PIL/ImagePalette.py
Normal file
257
.venv/lib/python3.9/site-packages/PIL/ImagePalette.py
Normal file
@ -0,0 +1,257 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import array
|
||||
|
||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||
|
||||
|
||||
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 and of length ``size``
|
||||
times the number of colors in ``mode``. The list must be aligned
|
||||
by channel (All R values must be contiguous in the list before G
|
||||
and B values.) Defaults to 0 through 255 per channel.
|
||||
:param size: An optional palette size. If given, it cannot be equal to
|
||||
or greater than 256. Defaults to 0.
|
||||
"""
|
||||
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
self.mode = mode
|
||||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or bytearray()
|
||||
self.dirty = None
|
||||
if size != 0 and size != len(self.palette):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
@property
|
||||
def palette(self):
|
||||
return self._palette
|
||||
|
||||
@palette.setter
|
||||
def palette(self, palette):
|
||||
self._palette = palette
|
||||
|
||||
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
|
||||
|
||||
def copy(self):
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""Convert palette to bytes.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
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 getcolor(self, color, image=None):
|
||||
"""Given an rgb tuple, allocate palette entry.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
if isinstance(color, tuple):
|
||||
if self.mode == "RGB":
|
||||
if len(color) == 4 and color[3] == 255:
|
||||
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
|
||||
if not isinstance(self.palette, bytearray):
|
||||
self._palette = bytearray(self.palette)
|
||||
index = len(self.palette) // 3
|
||||
special_colors = ()
|
||||
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:
|
||||
raise ValueError("cannot allocate more than 256 colors") from e
|
||||
self.colors[color] = index
|
||||
if index * 3 < len(self.palette):
|
||||
self._palette = (
|
||||
self.palette[: index * 3]
|
||||
+ bytes(color)
|
||||
+ self.palette[index * 3 + 3 :]
|
||||
)
|
||||
else:
|
||||
self._palette += bytes(color)
|
||||
self.dirty = 1
|
||||
return index
|
||||
else:
|
||||
raise ValueError(f"unknown color specifier: {repr(color)}")
|
||||
|
||||
def save(self, fp):
|
||||
"""Save palette to text file.
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
"""
|
||||
if self.rawmode:
|
||||
raise ValueError("palette contains raw palette data")
|
||||
if isinstance(fp, str):
|
||||
fp = open(fp, "w")
|
||||
fp.write("# Palette\n")
|
||||
fp.write(f"# Mode: {self.mode}\n")
|
||||
for i in range(256):
|
||||
fp.write(f"{i}")
|
||||
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
|
||||
try:
|
||||
fp.write(f" {self.palette[j]}")
|
||||
except IndexError:
|
||||
fp.write(" 0")
|
||||
fp.write("\n")
|
||||
fp.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Internal
|
||||
|
||||
|
||||
def raw(rawmode, data):
|
||||
palette = ImagePalette()
|
||||
palette.rawmode = rawmode
|
||||
palette.palette = data
|
||||
palette.dirty = 1
|
||||
return palette
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Factories
|
||||
|
||||
|
||||
def make_linear_lut(black, white):
|
||||
lut = []
|
||||
if black == 0:
|
||||
for i in range(256):
|
||||
lut.append(white * i // 255)
|
||||
else:
|
||||
raise NotImplementedError # FIXME
|
||||
return lut
|
||||
|
||||
|
||||
def make_gamma_lut(exp):
|
||||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
|
||||
return lut
|
||||
|
||||
|
||||
def negative(mode="RGB"):
|
||||
palette = list(range(256))
|
||||
palette.reverse()
|
||||
return ImagePalette(mode, palette * len(mode))
|
||||
|
||||
|
||||
def random(mode="RGB"):
|
||||
from random import randint
|
||||
|
||||
palette = []
|
||||
for i in range(256 * len(mode)):
|
||||
palette.append(randint(0, 255))
|
||||
return ImagePalette(mode, palette)
|
||||
|
||||
|
||||
def sepia(white="#fff0c0"):
|
||||
r, g, b = ImageColor.getrgb(white)
|
||||
r = make_linear_lut(0, r)
|
||||
g = make_linear_lut(0, g)
|
||||
b = make_linear_lut(0, b)
|
||||
return ImagePalette("RGB", r + g + b)
|
||||
|
||||
|
||||
def wedge(mode="RGB"):
|
||||
return ImagePalette(mode, list(range(256)) * len(mode))
|
||||
|
||||
|
||||
def load(filename):
|
||||
|
||||
# FIXME: supports GIMP gradients only
|
||||
|
||||
with open(filename, "rb") as fp:
|
||||
|
||||
for paletteHandler in [
|
||||
GimpPaletteFile.GimpPaletteFile,
|
||||
GimpGradientFile.GimpGradientFile,
|
||||
PaletteFile.PaletteFile,
|
||||
]:
|
||||
try:
|
||||
fp.seek(0)
|
||||
lut = paletteHandler(fp).getpalette()
|
||||
if lut:
|
||||
break
|
||||
except (SyntaxError, ValueError):
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
else:
|
||||
raise OSError("cannot load palette")
|
||||
|
||||
return lut # data, rawmode
|
19
.venv/lib/python3.9/site-packages/PIL/ImagePath.py
Normal file
19
.venv/lib/python3.9/site-packages/PIL/ImagePath.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
|
||||
Path = Image.core.path
|
213
.venv/lib/python3.9/site-packages/PIL/ImageQt.py
Normal file
213
.venv/lib/python3.9/site-packages/PIL/ImageQt.py
Normal file
@ -0,0 +1,213 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image
|
||||
from ._util import isPath
|
||||
|
||||
qt_versions = [
|
||||
["6", "PyQt6"],
|
||||
["side6", "PySide6"],
|
||||
["5", "PyQt5"],
|
||||
["side2", "PySide2"],
|
||||
]
|
||||
|
||||
# If a version has already been imported, attempt it first
|
||||
qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True)
|
||||
for qt_version, qt_module in qt_versions:
|
||||
try:
|
||||
if qt_module == "PyQt6":
|
||||
from PyQt6.QtCore import QBuffer, QIODevice
|
||||
from PyQt6.QtGui import QImage, QPixmap, qRgba
|
||||
elif qt_module == "PySide6":
|
||||
from PySide6.QtCore import QBuffer, QIODevice
|
||||
from PySide6.QtGui import QImage, QPixmap, qRgba
|
||||
elif qt_module == "PyQt5":
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
from PyQt5.QtGui import QImage, QPixmap, qRgba
|
||||
elif qt_module == "PySide2":
|
||||
from PySide2.QtCore import QBuffer, QIODevice
|
||||
from PySide2.QtGui import QImage, QPixmap, qRgba
|
||||
except (ImportError, RuntimeError):
|
||||
continue
|
||||
qt_is_installed = True
|
||||
break
|
||||
else:
|
||||
qt_is_installed = False
|
||||
qt_version = None
|
||||
|
||||
|
||||
def rgb(r, g, b, a=255):
|
||||
"""(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):
|
||||
"""
|
||||
:param im: QImage or PIL ImageQt object
|
||||
"""
|
||||
buffer = QBuffer()
|
||||
qt_openmode = QIODevice.OpenMode if qt_version == "6" else QIODevice
|
||||
buffer.open(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):
|
||||
return fromqimage(im)
|
||||
# buffer = QBuffer()
|
||||
# buffer.open(QIODevice.ReadWrite)
|
||||
# # im.save(buffer)
|
||||
# # What if png doesn't support some image features like animation?
|
||||
# im.save(buffer, 'ppm')
|
||||
# bytes_io = BytesIO()
|
||||
# bytes_io.write(buffer.data())
|
||||
# buffer.close()
|
||||
# bytes_io.seek(0)
|
||||
# return Image.open(bytes_io)
|
||||
|
||||
|
||||
def align8to32(bytes, width, mode):
|
||||
"""
|
||||
converts each scanline of data from 8 bit to 32 bit aligned
|
||||
"""
|
||||
|
||||
bits_per_pixel = {"1": 1, "L": 8, "P": 8}[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 = []
|
||||
for i in range(len(bytes) // bytes_per_line):
|
||||
new_data.append(
|
||||
bytes[i * bytes_per_line : (i + 1) * bytes_per_line]
|
||||
+ b"\x00" * extra_padding
|
||||
)
|
||||
|
||||
return b"".join(new_data)
|
||||
|
||||
|
||||
def _toqclass_helper(im):
|
||||
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 isPath(im):
|
||||
im = Image.open(im)
|
||||
exclusive_fp = True
|
||||
|
||||
qt_format = QImage.Format if qt_version == "6" else QImage
|
||||
if im.mode == "1":
|
||||
format = qt_format.Format_Mono
|
||||
elif im.mode == "L":
|
||||
format = qt_format.Format_Indexed8
|
||||
colortable = []
|
||||
for i in range(256):
|
||||
colortable.append(rgb(i, i, i))
|
||||
elif im.mode == "P":
|
||||
format = qt_format.Format_Indexed8
|
||||
colortable = []
|
||||
palette = im.getpalette()
|
||||
for i in range(0, len(palette), 3):
|
||||
colortable.append(rgb(*palette[i : i + 3]))
|
||||
elif im.mode == "RGB":
|
||||
# Populate the 4th channel with 255
|
||||
im = im.convert("RGBA")
|
||||
|
||||
data = im.tobytes("raw", "BGRA")
|
||||
format = qt_format.Format_RGB32
|
||||
elif im.mode == "RGBA":
|
||||
data = im.tobytes("raw", "BGRA")
|
||||
format = qt_format.Format_ARGB32
|
||||
else:
|
||||
if exclusive_fp:
|
||||
im.close()
|
||||
raise ValueError(f"unsupported image mode {repr(im.mode)}")
|
||||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
return ImageQt(im)
|
||||
|
||||
|
||||
def toqpixmap(im):
|
||||
# # This doesn't work. For now using a dumb approach.
|
||||
# im_data = _toqclass_helper(im)
|
||||
# result = QPixmap(im_data["size"][0], im_data["size"][1])
|
||||
# result.loadFromData(im_data["data"])
|
||||
qimage = toqimage(im)
|
||||
return QPixmap.fromImage(qimage)
|
75
.venv/lib/python3.9/site-packages/PIL/ImageSequence.py
Normal file
75
.venv/lib/python3.9/site-packages/PIL/ImageSequence.py
Normal file
@ -0,0 +1,75 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
##
|
||||
|
||||
|
||||
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):
|
||||
if not hasattr(im, "seek"):
|
||||
raise AttributeError("im must have seek method")
|
||||
self.im = im
|
||||
self.position = getattr(self.im, "_min_frame", 0)
|
||||
|
||||
def __getitem__(self, ix):
|
||||
try:
|
||||
self.im.seek(ix)
|
||||
return self.im
|
||||
except EOFError as e:
|
||||
raise IndexError from e # end of sequence
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
try:
|
||||
self.im.seek(self.position)
|
||||
self.position += 1
|
||||
return self.im
|
||||
except EOFError as e:
|
||||
raise StopIteration from e
|
||||
|
||||
|
||||
def all_frames(im, func=None):
|
||||
"""
|
||||
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
|
264
.venv/lib/python3.9/site-packages/PIL/ImageShow.py
Normal file
264
.venv/lib/python3.9/site-packages/PIL/ImageShow.py
Normal file
@ -0,0 +1,264 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from shlex import quote
|
||||
|
||||
from PIL import Image
|
||||
|
||||
_viewers = []
|
||||
|
||||
|
||||
def register(viewer, order=1):
|
||||
"""
|
||||
The :py:func:`register` function is used to register additional viewers.
|
||||
|
||||
: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.
|
||||
"""
|
||||
try:
|
||||
if issubclass(viewer, Viewer):
|
||||
viewer = viewer()
|
||||
except TypeError:
|
||||
pass # raised if viewer wasn't a class
|
||||
if order > 0:
|
||||
_viewers.append(viewer)
|
||||
else:
|
||||
_viewers.insert(0, viewer)
|
||||
|
||||
|
||||
def show(image, title=None, **options):
|
||||
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 1
|
||||
return 0
|
||||
|
||||
|
||||
class Viewer:
|
||||
"""Base class for viewers."""
|
||||
|
||||
# main api
|
||||
|
||||
def show(self, image, **options):
|
||||
"""
|
||||
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 = None
|
||||
"""The format to convert the image into."""
|
||||
options = {}
|
||||
"""Additional options used to convert the image."""
|
||||
|
||||
def get_format(self, image):
|
||||
"""Return format name, or ``None`` to save as PGM/PPM."""
|
||||
return self.format
|
||||
|
||||
def get_command(self, file, **options):
|
||||
"""
|
||||
Returns the command used to display the file.
|
||||
Not implemented in the base class.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save_image(self, image):
|
||||
"""Save to temporary file and return filename."""
|
||||
return image._dump(format=self.get_format(image), **self.options)
|
||||
|
||||
def show_image(self, image, **options):
|
||||
"""Display the given image."""
|
||||
return self.show_file(self.save_image(image), **options)
|
||||
|
||||
def show_file(self, file, **options):
|
||||
"""Display the given file."""
|
||||
os.system(self.get_command(file, **options))
|
||||
return 1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
class WindowsViewer(Viewer):
|
||||
"""The default viewer on Windows is the default system application for PNG files."""
|
||||
|
||||
format = "PNG"
|
||||
options = {"compress_level": 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
return (
|
||||
f'start "Pillow" /WAIT "{file}" '
|
||||
"&& ping -n 2 127.0.0.1 >NUL "
|
||||
f'&& del /f "{file}"'
|
||||
)
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
register(WindowsViewer)
|
||||
|
||||
|
||||
class MacViewer(Viewer):
|
||||
"""The default viewer on MacOS using ``Preview.app``."""
|
||||
|
||||
format = "PNG"
|
||||
options = {"compress_level": 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
# 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, file, **options):
|
||||
"""Display given file"""
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(file)
|
||||
with open(path) as f:
|
||||
subprocess.Popen(
|
||||
["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"],
|
||||
shell=True,
|
||||
stdin=f,
|
||||
)
|
||||
os.remove(path)
|
||||
return 1
|
||||
|
||||
|
||||
if sys.platform == "darwin":
|
||||
register(MacViewer)
|
||||
|
||||
|
||||
class UnixViewer(Viewer):
|
||||
format = "PNG"
|
||||
options = {"compress_level": 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
return f"({command} {quote(file)}; rm -f {quote(file)})&"
|
||||
|
||||
def show_file(self, file, **options):
|
||||
"""Display given file"""
|
||||
fd, path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(file)
|
||||
with open(path) as f:
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
subprocess.Popen(
|
||||
["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f
|
||||
)
|
||||
os.remove(path)
|
||||
return 1
|
||||
|
||||
|
||||
class DisplayViewer(UnixViewer):
|
||||
"""The ImageMagick ``display`` command."""
|
||||
|
||||
def get_command_ex(self, file, **options):
|
||||
command = executable = "display"
|
||||
return command, executable
|
||||
|
||||
|
||||
class GmDisplayViewer(UnixViewer):
|
||||
"""The GraphicsMagick ``gm display`` command."""
|
||||
|
||||
def get_command_ex(self, file, **options):
|
||||
executable = "gm"
|
||||
command = "gm display"
|
||||
return command, executable
|
||||
|
||||
|
||||
class EogViewer(UnixViewer):
|
||||
"""The GNOME Image Viewer ``eog`` command."""
|
||||
|
||||
def get_command_ex(self, file, **options):
|
||||
executable = "eog"
|
||||
command = "eog -n"
|
||||
return command, executable
|
||||
|
||||
|
||||
class XVViewer(UnixViewer):
|
||||
"""
|
||||
The X Viewer ``xv`` command.
|
||||
This viewer supports the ``title`` parameter.
|
||||
"""
|
||||
|
||||
def get_command_ex(self, file, title=None, **options):
|
||||
# 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
|
||||
|
||||
|
||||
if sys.platform not in ("win32", "darwin"): # unixoids
|
||||
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, **options):
|
||||
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:]))
|
147
.venv/lib/python3.9/site-packages/PIL/ImageStat.py
Normal file
147
.venv/lib/python3.9/site-packages/PIL/ImageStat.py
Normal file
@ -0,0 +1,147 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import functools
|
||||
import math
|
||||
import operator
|
||||
|
||||
|
||||
class Stat:
|
||||
def __init__(self, image_or_list, mask=None):
|
||||
try:
|
||||
if mask:
|
||||
self.h = image_or_list.histogram(mask)
|
||||
else:
|
||||
self.h = image_or_list.histogram()
|
||||
except AttributeError:
|
||||
self.h = image_or_list # assume it to be a histogram list
|
||||
if not isinstance(self.h, list):
|
||||
raise TypeError("first argument must be image or list")
|
||||
self.bands = list(range(len(self.h) // 256))
|
||||
|
||||
def __getattr__(self, id):
|
||||
"""Calculate missing attribute"""
|
||||
if id[:4] == "_get":
|
||||
raise AttributeError(id)
|
||||
# calculate missing attribute
|
||||
v = getattr(self, "_get" + id)()
|
||||
setattr(self, id, v)
|
||||
return v
|
||||
|
||||
def _getextrema(self):
|
||||
"""Get min/max values for each band in the image"""
|
||||
|
||||
def minmax(histogram):
|
||||
n = 255
|
||||
x = 0
|
||||
for i in range(256):
|
||||
if histogram[i]:
|
||||
n = min(n, i)
|
||||
x = max(x, i)
|
||||
return n, x # returns (255, 0) if there's no data in the histogram
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
v.append(minmax(self.h[i:]))
|
||||
return v
|
||||
|
||||
def _getcount(self):
|
||||
"""Get total number of pixels in each layer"""
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
v.append(functools.reduce(operator.add, self.h[i : i + 256]))
|
||||
return v
|
||||
|
||||
def _getsum(self):
|
||||
"""Get sum of all pixels in each layer"""
|
||||
|
||||
v = []
|
||||
for i in range(0, len(self.h), 256):
|
||||
layerSum = 0.0
|
||||
for j in range(256):
|
||||
layerSum += j * self.h[i + j]
|
||||
v.append(layerSum)
|
||||
return v
|
||||
|
||||
def _getsum2(self):
|
||||
"""Get squared sum of all pixels in each layer"""
|
||||
|
||||
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
|
||||
|
||||
def _getmean(self):
|
||||
"""Get average pixel level for each layer"""
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(self.sum[i] / self.count[i])
|
||||
return v
|
||||
|
||||
def _getmedian(self):
|
||||
"""Get median pixel level for each layer"""
|
||||
|
||||
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
|
||||
|
||||
def _getrms(self):
|
||||
"""Get RMS for each layer"""
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(math.sqrt(self.sum2[i] / self.count[i]))
|
||||
return v
|
||||
|
||||
def _getvar(self):
|
||||
"""Get variance for each layer"""
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
n = self.count[i]
|
||||
v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n)
|
||||
return v
|
||||
|
||||
def _getstddev(self):
|
||||
"""Get standard deviation for each layer"""
|
||||
|
||||
v = []
|
||||
for i in self.bands:
|
||||
v.append(math.sqrt(self.var[i]))
|
||||
return v
|
||||
|
||||
|
||||
Global = Stat # compatibility
|
300
.venv/lib/python3.9/site-packages/PIL/ImageTk.py
Normal file
300
.venv/lib/python3.9/site-packages/PIL/ImageTk.py
Normal file
@ -0,0 +1,300 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import tkinter
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Check for Tkinter interface hooks
|
||||
|
||||
_pilbitmap_ok = None
|
||||
|
||||
|
||||
def _pilbitmap_check():
|
||||
global _pilbitmap_ok
|
||||
if _pilbitmap_ok is None:
|
||||
try:
|
||||
im = Image.new("1", (1, 1))
|
||||
tkinter.BitmapImage(data=f"PIL:{im.im.id}")
|
||||
_pilbitmap_ok = 1
|
||||
except tkinter.TclError:
|
||||
_pilbitmap_ok = 0
|
||||
return _pilbitmap_ok
|
||||
|
||||
|
||||
def _get_image_from_kw(kw):
|
||||
source = None
|
||||
if "file" in kw:
|
||||
source = kw.pop("file")
|
||||
elif "data" in kw:
|
||||
source = BytesIO(kw.pop("data"))
|
||||
if source:
|
||||
return Image.open(source)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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=None, size=None, **kw):
|
||||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
image = _get_image_from_kw(kw)
|
||||
|
||||
if hasattr(image, "mode") and hasattr(image, "size"):
|
||||
# got an image instead of a mode
|
||||
mode = image.mode
|
||||
if mode == "P":
|
||||
# palette mapped data
|
||||
image.load()
|
||||
try:
|
||||
mode = image.palette.mode
|
||||
except AttributeError:
|
||||
mode = "RGB" # default
|
||||
size = image.size
|
||||
kw["width"], kw["height"] = size
|
||||
else:
|
||||
mode = image
|
||||
image = None
|
||||
|
||||
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):
|
||||
name = self.__photo.name
|
||||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Get the width of the image.
|
||||
|
||||
:return: The width, in pixels.
|
||||
"""
|
||||
return self.__size[0]
|
||||
|
||||
def height(self):
|
||||
"""
|
||||
Get the height of the image.
|
||||
|
||||
:return: The height, in pixels.
|
||||
"""
|
||||
return self.__size[1]
|
||||
|
||||
def paste(self, im, box=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.
|
||||
: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.
|
||||
"""
|
||||
|
||||
# convert to blittable
|
||||
im.load()
|
||||
image = im.im
|
||||
if image.isblock() and im.mode == self.__mode:
|
||||
block = image
|
||||
else:
|
||||
block = image.new_block(self.__mode, im.size)
|
||||
image.convert2(block, image) # convert directly between buffers
|
||||
|
||||
tk = self.__photo.tk
|
||||
|
||||
try:
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except tkinter.TclError:
|
||||
# activate Tkinter hook
|
||||
try:
|
||||
from . import _imagingtk
|
||||
|
||||
try:
|
||||
if hasattr(tk, "interp"):
|
||||
# Required for PyPy, which always has CFFI installed
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
# PyPy is using an FFI CDATA element
|
||||
# (Pdb) self.tk.interp
|
||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||
else:
|
||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||
except AttributeError:
|
||||
_imagingtk.tkinit(id(tk), 0)
|
||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
||||
except (ImportError, AttributeError, tkinter.TclError):
|
||||
raise # configuration problem; cannot attach to Tkinter
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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=None, **kw):
|
||||
|
||||
# Tk compatibility: file or data
|
||||
if image is None:
|
||||
image = _get_image_from_kw(kw)
|
||||
|
||||
self.__mode = image.mode
|
||||
self.__size = image.size
|
||||
|
||||
if _pilbitmap_check():
|
||||
# fast way (requires the pilbitmap booster patch)
|
||||
image.load()
|
||||
kw["data"] = f"PIL:{image.im.id}"
|
||||
self.__im = image # must keep a reference
|
||||
else:
|
||||
# slow but safe way
|
||||
kw["data"] = image.tobitmap()
|
||||
self.__photo = tkinter.BitmapImage(**kw)
|
||||
|
||||
def __del__(self):
|
||||
name = self.__photo.name
|
||||
self.__photo.name = None
|
||||
try:
|
||||
self.__photo.tk.call("image", "delete", name)
|
||||
except Exception:
|
||||
pass # ignore internal errors
|
||||
|
||||
def width(self):
|
||||
"""
|
||||
Get the width of the image.
|
||||
|
||||
:return: The width, in pixels.
|
||||
"""
|
||||
return self.__size[0]
|
||||
|
||||
def height(self):
|
||||
"""
|
||||
Get the height of the image.
|
||||
|
||||
:return: The height, in pixels.
|
||||
"""
|
||||
return self.__size[1]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
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):
|
||||
"""Copies the contents of a PhotoImage to a PIL image memory."""
|
||||
im = Image.new("RGBA", (photo.width(), photo.height()))
|
||||
block = im.im
|
||||
|
||||
photo.tk.call("PyImagingPhotoGet", photo, block.id)
|
||||
|
||||
return im
|
||||
|
||||
|
||||
def _show(image, title):
|
||||
"""Helper for the Image.show method."""
|
||||
|
||||
class UI(tkinter.Label):
|
||||
def __init__(self, master, im):
|
||||
if im.mode == "1":
|
||||
self.image = BitmapImage(im, foreground="white", master=master)
|
||||
else:
|
||||
self.image = PhotoImage(im, master=master)
|
||||
super().__init__(master, image=self.image, bg="black", bd=0)
|
||||
|
||||
if not tkinter._default_root:
|
||||
raise OSError("tkinter not initialized")
|
||||
top = tkinter.Toplevel()
|
||||
if title:
|
||||
top.title(title)
|
||||
UI(top, image).pack()
|
102
.venv/lib/python3.9/site-packages/PIL/ImageTransform.py
Normal file
102
.venv/lib/python3.9/site-packages/PIL/ImageTransform.py
Normal file
@ -0,0 +1,102 @@
|
||||
#
|
||||
# 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 . import Image
|
||||
|
||||
|
||||
class Transform(Image.ImageTransformHandler):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def getdata(self):
|
||||
return self.method, self.data
|
||||
|
||||
def transform(self, size, image, **options):
|
||||
# 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 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:`~PIL.Image.Image.transform`
|
||||
|
||||
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
|
||||
from an affine transform matrix.
|
||||
"""
|
||||
|
||||
method = Image.AFFINE
|
||||
|
||||
|
||||
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:`~PIL.Image.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.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:`~PIL.Image.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.QUAD
|
||||
|
||||
|
||||
class MeshTransform(Transform):
|
||||
"""
|
||||
Define a mesh image transform. A mesh transform consists of one or more
|
||||
individual quad transforms.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.transform`
|
||||
|
||||
:param data: A list of (bbox, quad) tuples.
|
||||
"""
|
||||
|
||||
method = Image.MESH
|
230
.venv/lib/python3.9/site-packages/PIL/ImageWin.py
Normal file
230
.venv/lib/python3.9/site-packages/PIL/ImageWin.py
Normal file
@ -0,0 +1,230 @@
|
||||
#
|
||||
# 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 . 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):
|
||||
self.dc = dc
|
||||
|
||||
def __int__(self):
|
||||
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):
|
||||
self.wnd = wnd
|
||||
|
||||
def __int__(self):
|
||||
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 greylevels
|
||||
are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together
|
||||
with 20 greylevels.
|
||||
|
||||
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, size=None):
|
||||
if hasattr(image, "mode") and hasattr(image, "size"):
|
||||
mode = image.mode
|
||||
size = image.size
|
||||
else:
|
||||
mode = image
|
||||
image = None
|
||||
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:
|
||||
self.paste(image)
|
||||
|
||||
def expose(self, handle):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if isinstance(handle, HWND):
|
||||
dc = self.image.getdc(handle)
|
||||
try:
|
||||
result = self.image.expose(dc)
|
||||
finally:
|
||||
self.image.releasedc(handle, dc)
|
||||
else:
|
||||
result = self.image.expose(handle)
|
||||
return result
|
||||
|
||||
def draw(self, handle, dst, src=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 not src:
|
||||
src = (0, 0) + self.size
|
||||
if isinstance(handle, HWND):
|
||||
dc = self.image.getdc(handle)
|
||||
try:
|
||||
result = self.image.draw(dc, dst, src)
|
||||
finally:
|
||||
self.image.releasedc(handle, dc)
|
||||
else:
|
||||
result = self.image.draw(handle, dst, src)
|
||||
return result
|
||||
|
||||
def query_palette(self, handle):
|
||||
"""
|
||||
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: A true value if one or more entries were changed (this
|
||||
indicates that the image should be redrawn).
|
||||
"""
|
||||
if isinstance(handle, HWND):
|
||||
handle = self.image.getdc(handle)
|
||||
try:
|
||||
result = self.image.query_palette(handle)
|
||||
finally:
|
||||
self.image.releasedc(handle, handle)
|
||||
else:
|
||||
result = self.image.query_palette(handle)
|
||||
return result
|
||||
|
||||
def paste(self, im, box=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):
|
||||
"""
|
||||
Load display memory contents from byte data.
|
||||
|
||||
:param buffer: A buffer containing display data (usually
|
||||
data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`)
|
||||
"""
|
||||
return self.image.frombytes(buffer)
|
||||
|
||||
def tobytes(self):
|
||||
"""
|
||||
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="PIL", width=None, height=None):
|
||||
self.hwnd = Image.core.createwindow(
|
||||
title, self.__dispatcher, width or 0, height or 0
|
||||
)
|
||||
|
||||
def __dispatcher(self, action, *args):
|
||||
return getattr(self, "ui_handle_" + action)(*args)
|
||||
|
||||
def ui_handle_clear(self, dc, x0, y0, x1, y1):
|
||||
pass
|
||||
|
||||
def ui_handle_damage(self, x0, y0, x1, y1):
|
||||
pass
|
||||
|
||||
def ui_handle_destroy(self):
|
||||
pass
|
||||
|
||||
def ui_handle_repair(self, dc, x0, y0, x1, y1):
|
||||
pass
|
||||
|
||||
def ui_handle_resize(self, width, height):
|
||||
pass
|
||||
|
||||
def mainloop(self):
|
||||
Image.core.eventloop()
|
||||
|
||||
|
||||
class ImageWindow(Window):
|
||||
"""Create an image window which displays the given image."""
|
||||
|
||||
def __init__(self, image, title="PIL"):
|
||||
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, x0, y0, x1, y1):
|
||||
self.image.draw(dc, (x0, y0, x1, y1))
|
93
.venv/lib/python3.9/site-packages/PIL/ImtImagePlugin.py
Normal file
93
.venv/lib/python3.9/site-packages/PIL/ImtImagePlugin.py
Normal file
@ -0,0 +1,93 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
field = re.compile(br"([a-z]*) ([^ \r\n]*)")
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for IM Tools images.
|
||||
|
||||
|
||||
class ImtImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "IMT"
|
||||
format_description = "IM Tools"
|
||||
|
||||
def _open(self):
|
||||
|
||||
# Quick rejection: if there's not a LF among the first
|
||||
# 100 bytes, this is (probably) not a text header.
|
||||
|
||||
if b"\n" not in self.fp.read(100):
|
||||
raise SyntaxError("not an IM file")
|
||||
self.fp.seek(0)
|
||||
|
||||
xsize = ysize = 0
|
||||
|
||||
while True:
|
||||
|
||||
s = self.fp.read(1)
|
||||
if not s:
|
||||
break
|
||||
|
||||
if s == b"\x0C":
|
||||
|
||||
# image data begins
|
||||
self.tile = [
|
||||
("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))
|
||||
]
|
||||
|
||||
break
|
||||
|
||||
else:
|
||||
|
||||
# read key/value pair
|
||||
# FIXME: dangerous, may read whole file
|
||||
s = s + self.fp.readline()
|
||||
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 == "width":
|
||||
xsize = int(v)
|
||||
self._size = xsize, ysize
|
||||
elif k == "height":
|
||||
ysize = int(v)
|
||||
self._size = xsize, ysize
|
||||
elif k == "pixel" and v == "n8":
|
||||
self.mode = "L"
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(ImtImageFile.format, ImtImageFile)
|
||||
|
||||
#
|
||||
# no extension registered (".im" is simply too common)
|
230
.venv/lib/python3.9/site-packages/PIL/IptcImagePlugin.py
Normal file
230
.venv/lib/python3.9/site-packages/PIL/IptcImagePlugin.py
Normal file
@ -0,0 +1,230 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
from ._binary import o8
|
||||
|
||||
COMPRESSION = {1: "raw", 5: "jpeg"}
|
||||
|
||||
PAD = o8(0) * 4
|
||||
|
||||
|
||||
#
|
||||
# Helpers
|
||||
|
||||
|
||||
def i(c):
|
||||
return i32((PAD + c)[-4:])
|
||||
|
||||
|
||||
def dump(c):
|
||||
for i in c:
|
||||
print("%02x" % i8(i), end=" ")
|
||||
print()
|
||||
|
||||
|
||||
##
|
||||
# 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):
|
||||
return i(self.info[key])
|
||||
|
||||
def field(self):
|
||||
#
|
||||
# get a IPTC field header
|
||||
s = self.fp.read(5)
|
||||
if not len(s):
|
||||
return None, 0
|
||||
|
||||
tag = s[1], s[2]
|
||||
|
||||
# syntax
|
||||
if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
|
||||
raise SyntaxError("invalid IPTC/NAA file")
|
||||
|
||||
# field size
|
||||
size = s[3]
|
||||
if size > 132:
|
||||
raise OSError("illegal field length in IPTC/NAA file")
|
||||
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):
|
||||
|
||||
# load descriptive fields
|
||||
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 = i8(self.info[(3, 60)][0])
|
||||
component = i8(self.info[(3, 60)][1])
|
||||
if (3, 65) in self.info:
|
||||
id = i8(self.info[(3, 65)][0]) - 1
|
||||
else:
|
||||
id = 0
|
||||
if layers == 1 and not component:
|
||||
self.mode = "L"
|
||||
elif layers == 3 and component:
|
||||
self.mode = "RGB"[id]
|
||||
elif layers == 4 and component:
|
||||
self.mode = "CMYK"[id]
|
||||
|
||||
# size
|
||||
self._size = self.getint((3, 20)), self.getint((3, 30))
|
||||
|
||||
# compression
|
||||
try:
|
||||
compression = COMPRESSION[self.getint((3, 120))]
|
||||
except KeyError as e:
|
||||
raise OSError("Unknown IPTC image compression") from e
|
||||
|
||||
# tile
|
||||
if tag == (8, 10):
|
||||
self.tile = [
|
||||
("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
|
||||
]
|
||||
|
||||
def load(self):
|
||||
|
||||
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
type, tile, box = self.tile[0]
|
||||
|
||||
encoding, offset = tile
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# Copy image data to temporary file
|
||||
o_fd, outfile = tempfile.mkstemp(text=False)
|
||||
o = os.fdopen(o_fd)
|
||||
if encoding == "raw":
|
||||
# To simplify access to the extracted file,
|
||||
# prepend a PPM header
|
||||
o.write("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)
|
||||
o.close()
|
||||
|
||||
try:
|
||||
with Image.open(outfile) as _im:
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
finally:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||
|
||||
Image.register_extension(IptcImageFile.format, ".iim")
|
||||
|
||||
|
||||
def getiptcinfo(im):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
import io
|
||||
|
||||
from . import JpegImagePlugin, TiffImagePlugin
|
||||
|
||||
data = None
|
||||
|
||||
if isinstance(im, IptcImageFile):
|
||||
# return info dictionary right away
|
||||
return im.info
|
||||
|
||||
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.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
if data is None:
|
||||
return None # no properties
|
||||
|
||||
# create an IptcImagePlugin object without initializing it
|
||||
class FakeImage:
|
||||
pass
|
||||
|
||||
im = FakeImage()
|
||||
im.__class__ = IptcImageFile
|
||||
|
||||
# parse the IPTC information chunk
|
||||
im.info = {}
|
||||
im.fp = io.BytesIO(data)
|
||||
|
||||
try:
|
||||
im._open()
|
||||
except (IndexError, KeyError):
|
||||
pass # expected failure
|
||||
|
||||
return im.info
|
314
.venv/lib/python3.9/site-packages/PIL/Jpeg2KImagePlugin.py
Normal file
314
.venv/lib/python3.9/site-packages/PIL/Jpeg2KImagePlugin.py
Normal file
@ -0,0 +1,314 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# $Id$
|
||||
#
|
||||
# JPEG2000 file handling
|
||||
#
|
||||
# History:
|
||||
# 2014-03-12 ajh Created
|
||||
#
|
||||
# Copyright (c) 2014 Coriolis Systems Limited
|
||||
# Copyright (c) 2014 Alastair Houghton
|
||||
#
|
||||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
|
||||
def _parse_codestream(fp):
|
||||
"""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 = struct.unpack(">H", hdr)[0]
|
||||
siz = hdr + fp.read(lsiz - 2)
|
||||
lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
|
||||
">HHIIIIIIIIH", siz
|
||||
)
|
||||
ssiz = [None] * csiz
|
||||
xrsiz = [None] * csiz
|
||||
yrsiz = [None] * csiz
|
||||
for i in range(csiz):
|
||||
ssiz[i], xrsiz[i], yrsiz[i] = struct.unpack_from(">BBB", siz, 36 + 3 * i)
|
||||
|
||||
size = (xsiz - xosiz, ysiz - yosiz)
|
||||
if csiz == 1:
|
||||
if (yrsiz[0] & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
else:
|
||||
mode = "L"
|
||||
elif csiz == 2:
|
||||
mode = "LA"
|
||||
elif csiz == 3:
|
||||
mode = "RGB"
|
||||
elif csiz == 4:
|
||||
mode = "RGBA"
|
||||
else:
|
||||
mode = None
|
||||
|
||||
return (size, mode)
|
||||
|
||||
|
||||
def _parse_jp2_header(fp):
|
||||
"""Parse the JP2 header box to extract size, component count and
|
||||
color space information, returning a (size, mode, mimetype) tuple."""
|
||||
|
||||
# Find the JP2 header box
|
||||
header = None
|
||||
mimetype = None
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", fp.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", fp.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen:
|
||||
raise SyntaxError("Invalid JP2 header length")
|
||||
|
||||
if tbox == b"jp2h":
|
||||
header = fp.read(lbox - hlen)
|
||||
break
|
||||
elif tbox == b"ftyp":
|
||||
if fp.read(4) == b"jpx ":
|
||||
mimetype = "image/jpx"
|
||||
fp.seek(lbox - hlen - 4, os.SEEK_CUR)
|
||||
else:
|
||||
fp.seek(lbox - hlen, os.SEEK_CUR)
|
||||
|
||||
if header is None:
|
||||
raise SyntaxError("could not find JP2 header")
|
||||
|
||||
size = None
|
||||
mode = None
|
||||
bpc = None
|
||||
nc = None
|
||||
|
||||
hio = io.BytesIO(header)
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", hio.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", hio.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
content = hio.read(lbox - hlen)
|
||||
|
||||
if tbox == b"ihdr":
|
||||
height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content)
|
||||
size = (width, height)
|
||||
if unkc:
|
||||
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"
|
||||
break
|
||||
elif tbox == b"colr":
|
||||
meth, prec, approx = struct.unpack_from(">BBB", content)
|
||||
if meth == 1:
|
||||
cs = struct.unpack_from(">I", content, 3)[0]
|
||||
if cs == 16: # sRGB
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
elif cs == 17: # grayscale
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
elif nc == 1:
|
||||
mode = "L"
|
||||
elif nc == 2:
|
||||
mode = "LA"
|
||||
break
|
||||
elif cs == 18: # sYCC
|
||||
if nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
|
||||
if size is None or mode is None:
|
||||
raise SyntaxError("Malformed jp2 header")
|
||||
|
||||
return (size, mode, mimetype)
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for JPEG2000 images.
|
||||
|
||||
|
||||
class Jpeg2KImageFile(ImageFile.ImageFile):
|
||||
format = "JPEG2000"
|
||||
format_description = "JPEG 2000 (ISO 15444)"
|
||||
|
||||
def _open(self):
|
||||
sig = self.fp.read(4)
|
||||
if sig == b"\xff\x4f\xff\x51":
|
||||
self.codec = "j2k"
|
||||
self._size, self.mode = _parse_codestream(self.fp)
|
||||
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 = header
|
||||
else:
|
||||
raise SyntaxError("not a JPEG 2000 file")
|
||||
|
||||
if self.size is None or self.mode is None:
|
||||
raise SyntaxError("unable to determine size/mode")
|
||||
|
||||
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 = [
|
||||
(
|
||||
"jpeg2k",
|
||||
(0, 0) + self.size,
|
||||
0,
|
||||
(self.codec, self._reduce, self.layers, fd, length),
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def reduce(self):
|
||||
# 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):
|
||||
self._reduce = value
|
||||
|
||||
def load(self):
|
||||
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]
|
||||
t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
|
||||
self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
|
||||
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return (
|
||||
prefix[:4] == b"\xff\x4f\xff\x51"
|
||||
or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Save support
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if filename.endswith(".j2k"):
|
||||
kind = "j2k"
|
||||
else:
|
||||
kind = "jp2"
|
||||
|
||||
# Get the keyword arguments
|
||||
info = im.encoderinfo
|
||||
|
||||
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
|
||||
]
|
||||
)
|
||||
):
|
||||
raise ValueError("quality_layers must be a sequence of numbers")
|
||||
|
||||
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")
|
||||
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,
|
||||
fd,
|
||||
)
|
||||
|
||||
ImageFile._save(im, fp, [("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")
|
826
.venv/lib/python3.9/site-packages/PIL/JpegImagePlugin.py
Normal file
826
.venv/lib/python3.9/site-packages/PIL/JpegImagePlugin.py
Normal file
@ -0,0 +1,826 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
import array
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
|
||||
from . import Image, ImageFile, TiffImagePlugin
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
from ._binary import o8
|
||||
from .JpegPresets import presets
|
||||
|
||||
#
|
||||
# Parser
|
||||
|
||||
|
||||
def Skip(self, marker):
|
||||
n = i16(self.fp.read(2)) - 2
|
||||
ImageFile._safe_read(self.fp, n)
|
||||
|
||||
|
||||
def APP(self, marker):
|
||||
#
|
||||
# Application marker. Store these in the APP dictionary.
|
||||
# Also look for well-known application markers.
|
||||
|
||||
n = i16(self.fp.read(2)) - 2
|
||||
s = ImageFile._safe_read(self.fp, n)
|
||||
|
||||
app = "APP%d" % (marker & 15)
|
||||
|
||||
self.app[app] = s # compatibility
|
||||
self.applist.append((app, s))
|
||||
|
||||
if marker == 0xFFE0 and s[:4] == 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
|
||||
self.info["jfif_unit"] = jfif_unit
|
||||
self.info["jfif_density"] = jfif_density
|
||||
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
|
||||
if "exif" not in self.info:
|
||||
# extract EXIF information (incomplete)
|
||||
self.info["exif"] = s # FIXME: value will change
|
||||
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
||||
# extract FlashPix information (incomplete)
|
||||
self.info["flashpix"] = s # FIXME: value will change
|
||||
elif marker == 0xFFE2 and s[:12] == 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[:14] == b"Photoshop 3.0\x00":
|
||||
# parse the image resource block
|
||||
offset = 14
|
||||
photoshop = self.info.setdefault("photoshop", {})
|
||||
while s[offset : offset + 4] == b"8BIM":
|
||||
try:
|
||||
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
|
||||
data = {
|
||||
"XResolution": i32(data, 0) / 65536,
|
||||
"DisplayedUnitsX": i16(data, 4),
|
||||
"YResolution": i32(data, 8) / 65536,
|
||||
"DisplayedUnitsY": i16(data, 12),
|
||||
}
|
||||
photoshop[code] = data
|
||||
offset += size
|
||||
offset += offset & 1 # align
|
||||
except struct.error:
|
||||
break # insufficient data
|
||||
|
||||
elif marker == 0xFFEE and s[:5] == 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[:4] == 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
|
||||
|
||||
# If DPI isn't in JPEG header, fetch from EXIF
|
||||
if "dpi" not in self.info and "exif" in self.info:
|
||||
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):
|
||||
raise ValueError
|
||||
if resolution_unit == 3: # cm
|
||||
# 1 dpcm = 2.54 dpi
|
||||
dpi *= 2.54
|
||||
self.info["dpi"] = dpi, dpi
|
||||
except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||
# SyntaxError for invalid/unreadable EXIF
|
||||
# KeyError for dpi not included
|
||||
# ZeroDivisionError for invalid dpi rational value
|
||||
# ValueError for dpi being an invalid float
|
||||
self.info["dpi"] = 72, 72
|
||||
|
||||
|
||||
def COM(self, marker):
|
||||
#
|
||||
# Comment marker. Store these in the APP dictionary.
|
||||
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, marker):
|
||||
#
|
||||
# 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.
|
||||
|
||||
n = i16(self.fp.read(2)) - 2
|
||||
s = ImageFile._safe_read(self.fp, n)
|
||||
self._size = i16(s, 3), i16(s, 1)
|
||||
|
||||
self.bits = s[0]
|
||||
if self.bits != 8:
|
||||
raise SyntaxError(f"cannot handle {self.bits}-bit layers")
|
||||
|
||||
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:
|
||||
raise SyntaxError(f"cannot handle {self.layers}-layer images")
|
||||
|
||||
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 = []
|
||||
for p in self.icclist:
|
||||
profile.append(p[14:])
|
||||
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, marker):
|
||||
#
|
||||
# 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.
|
||||
|
||||
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:
|
||||
raise SyntaxError("bad quantization table marker")
|
||||
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):
|
||||
# Magic number was taken from https://en.wikipedia.org/wiki/JPEG
|
||||
return prefix[0:3] == 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):
|
||||
|
||||
s = self.fp.read(3)
|
||||
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a JPEG file")
|
||||
s = b"\xFF"
|
||||
|
||||
# Create attributes
|
||||
self.bits = self.layers = 0
|
||||
|
||||
# JPEG specifics (internal)
|
||||
self.layer = []
|
||||
self.huffman_dc = {}
|
||||
self.huffman_ac = {}
|
||||
self.quantization = {}
|
||||
self.app = {} # compatibility
|
||||
self.applist = []
|
||||
self.icclist = []
|
||||
|
||||
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 = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))]
|
||||
# self.__offset = self.fp.tell()
|
||||
break
|
||||
s = self.fp.read(1)
|
||||
elif i == 0 or i == 0xFFFF:
|
||||
# padded marker or junk; move on
|
||||
s = b"\xff"
|
||||
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
||||
s = self.fp.read(1)
|
||||
else:
|
||||
raise SyntaxError("no marker found")
|
||||
|
||||
def load_read(self, read_bytes):
|
||||
"""
|
||||
internal: read more image data
|
||||
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
|
||||
so libjpeg can finish decoding
|
||||
"""
|
||||
s = self.fp.read(read_bytes)
|
||||
|
||||
if not s and ImageFile.LOAD_TRUNCATED_IMAGES:
|
||||
# Premature EOF.
|
||||
# Pretend file is finished adding EOI marker
|
||||
return b"\xFF\xD9"
|
||||
|
||||
return s
|
||||
|
||||
def draft(self, mode, size):
|
||||
|
||||
if len(self.tile) != 1:
|
||||
return
|
||||
|
||||
# Protect from second call
|
||||
if self.decoderconfig:
|
||||
return
|
||||
|
||||
d, e, o, a = self.tile[0]
|
||||
scale = 1
|
||||
original_size = self.size
|
||||
|
||||
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
|
||||
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 = [(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):
|
||||
|
||||
# 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:
|
||||
raise ValueError("Invalid Filename")
|
||||
|
||||
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):
|
||||
return _getexif(self)
|
||||
|
||||
def _getmp(self):
|
||||
return _getmp(self)
|
||||
|
||||
def getxmp(self):
|
||||
"""
|
||||
Returns a dictionary containing the XMP tags.
|
||||
Requires defusedxml to be installed.
|
||||
:returns: XMP tags in a dictionary.
|
||||
"""
|
||||
|
||||
for segment, content in self.applist:
|
||||
if segment == "APP1":
|
||||
marker, xmp_tags = content.rsplit(b"\x00", 1)
|
||||
if marker == b"http://ns.adobe.com/xap/1.0/":
|
||||
return self._getxmp(xmp_tags)
|
||||
return {}
|
||||
|
||||
|
||||
def _getexif(self):
|
||||
if "exif" not in self.info:
|
||||
return None
|
||||
return self.getexif()._get_merged_dict()
|
||||
|
||||
|
||||
def _getmp(self):
|
||||
# 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[:4] == b"\x4d\x4d\x00\x2a" else "<"
|
||||
# process dictionary
|
||||
try:
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||
file_contents.seek(info.next)
|
||||
info.load(file_contents)
|
||||
mp = dict(info)
|
||||
except Exception as e:
|
||||
raise SyntaxError("malformed MP Index (unreadable directory)") from e
|
||||
# it's an error not to have a number of images
|
||||
try:
|
||||
quant = mp[0xB001]
|
||||
except KeyError as e:
|
||||
raise SyntaxError("malformed MP Index (no number of images)") from e
|
||||
# get MP entries
|
||||
mpentries = []
|
||||
try:
|
||||
rawmpentries = mp[0xB002]
|
||||
for entrynum in range(0, 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:
|
||||
raise SyntaxError("unsupported picture format in MPO")
|
||||
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:
|
||||
raise SyntaxError("malformed MP Index (bad MP Entry)") 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 convert_dict_qtables(qtables):
|
||||
warnings.warn(
|
||||
"convert_dict_qtables is deprecated and will be removed in Pillow 10"
|
||||
"(2023-01-02). Conversion is no longer needed.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return qtables
|
||||
|
||||
|
||||
def get_sampling(im):
|
||||
# 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 hasattr(im, "layers") 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, fp, filename):
|
||||
|
||||
try:
|
||||
rawmode = RAWMODE[im.mode]
|
||||
except KeyError as e:
|
||||
raise OSError(f"cannot write mode {im.mode} as JPEG") 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):
|
||||
raise ValueError("Invalid quality setting")
|
||||
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":
|
||||
raise ValueError("Cannot use 'keep' when original image is not a JPEG")
|
||||
subsampling = get_sampling(im)
|
||||
|
||||
def validate_qtables(qtables):
|
||||
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:
|
||||
raise ValueError("Invalid quantization table") 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):
|
||||
raise ValueError("None or too many quantization tables")
|
||||
for idx, table in enumerate(qtables):
|
||||
try:
|
||||
if len(table) != 64:
|
||||
raise TypeError
|
||||
table = array.array("H", table)
|
||||
except TypeError as e:
|
||||
raise ValueError("Invalid quantization table") from e
|
||||
else:
|
||||
qtables[idx] = list(table)
|
||||
return qtables
|
||||
|
||||
if qtables == "keep":
|
||||
if im.format != "JPEG":
|
||||
raise ValueError("Cannot use 'keep' when original image is not a JPEG")
|
||||
qtables = getattr(im, "quantization", None)
|
||||
qtables = validate_qtables(qtables)
|
||||
|
||||
extra = b""
|
||||
|
||||
icc_profile = info.get("icc_profile")
|
||||
if icc_profile:
|
||||
ICC_OVERHEAD_LEN = 14
|
||||
MAX_BYTES_IN_MARKER = 65533
|
||||
MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_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 = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
|
||||
extra += (
|
||||
b"\xFF\xE2"
|
||||
+ size
|
||||
+ b"ICC_PROFILE\0"
|
||||
+ o8(i)
|
||||
+ o8(len(markers))
|
||||
+ marker
|
||||
)
|
||||
i += 1
|
||||
|
||||
# "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()
|
||||
|
||||
# get keyword arguments
|
||||
im.encoderconfig = (
|
||||
quality,
|
||||
progressive,
|
||||
info.get("smooth", 0),
|
||||
optimize,
|
||||
info.get("streamtype", 0),
|
||||
dpi[0],
|
||||
dpi[1],
|
||||
subsampling,
|
||||
qtables,
|
||||
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
|
||||
bufsize = 0
|
||||
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]
|
||||
|
||||
# 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(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1)
|
||||
|
||||
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize)
|
||||
|
||||
|
||||
def _save_cjpeg(im, fp, filename):
|
||||
# ALTERNATIVE: handle JPEGs via the IJG command line utilities.
|
||||
tempfile = im._dump()
|
||||
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
|
||||
try:
|
||||
os.unlink(tempfile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
##
|
||||
# Factory for making JPEG and MPO instances
|
||||
def jpeg_factory(fp=None, filename=None):
|
||||
im = JpegImageFile(fp, filename)
|
||||
try:
|
||||
mpheader = im._getmp()
|
||||
if mpheader[45057] > 1:
|
||||
# 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")
|
240
.venv/lib/python3.9/site-packages/PIL/JpegPresets.py
Normal file
240
.venv/lib/python3.9/site-packages/PIL/JpegPresets.py
Normal file
@ -0,0 +1,240 @@
|
||||
"""
|
||||
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://www.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
|
||||
|
||||
"""
|
||||
|
||||
# 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
|
75
.venv/lib/python3.9/site-packages/PIL/McIdasImagePlugin.py
Normal file
75
.venv/lib/python3.9/site-packages/PIL/McIdasImagePlugin.py
Normal file
@ -0,0 +1,75 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import struct
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
||||
|
||||
def _accept(s):
|
||||
return s[:8] == 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):
|
||||
|
||||
# parse area file directory
|
||||
s = self.fp.read(256)
|
||||
if not _accept(s) or len(s) != 256:
|
||||
raise SyntaxError("not an McIdas area file")
|
||||
|
||||
self.area_descriptor_raw = s
|
||||
self.area_descriptor = w = [0] + list(struct.unpack("!64i", s))
|
||||
|
||||
# get mode
|
||||
if w[11] == 1:
|
||||
mode = rawmode = "L"
|
||||
elif w[11] == 2:
|
||||
# FIXME: add memory map support
|
||||
mode = "I"
|
||||
rawmode = "I;16B"
|
||||
elif w[11] == 4:
|
||||
# FIXME: add memory map support
|
||||
mode = "I"
|
||||
rawmode = "I;32B"
|
||||
else:
|
||||
raise SyntaxError("unsupported McIdas format")
|
||||
|
||||
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 = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# registry
|
||||
|
||||
Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept)
|
||||
|
||||
# no default extension
|
107
.venv/lib/python3.9/site-packages/PIL/MicImagePlugin.py
Normal file
107
.venv/lib/python3.9/site-packages/PIL/MicImagePlugin.py
Normal file
@ -0,0 +1,107 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import olefile
|
||||
|
||||
from . import Image, TiffImagePlugin
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:8] == 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):
|
||||
|
||||
# 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:
|
||||
raise SyntaxError("not an MIC file; invalid OLE file") from e
|
||||
|
||||
# find ACI subfiles with Image members (maybe not the
|
||||
# best way to identify MIC files, but what the... ;-)
|
||||
|
||||
self.images = []
|
||||
for path in self.ole.listdir():
|
||||
if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image":
|
||||
self.images.append(path)
|
||||
|
||||
# if we didn't find any images, this is probably not
|
||||
# an MIC file.
|
||||
if not self.images:
|
||||
raise SyntaxError("not an MIC file; no image entries")
|
||||
|
||||
self.__fp = self.fp
|
||||
self.frame = None
|
||||
self._n_frames = len(self.images)
|
||||
self.is_animated = self._n_frames > 1
|
||||
|
||||
if len(self.images) > 1:
|
||||
self._category = Image.CONTAINER
|
||||
|
||||
self.seek(0)
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
try:
|
||||
filename = self.images[frame]
|
||||
except IndexError as e:
|
||||
raise EOFError("no such frame") from e
|
||||
|
||||
self.fp = self.ole.openstream(filename)
|
||||
|
||||
TiffImagePlugin.TiffImageFile._open(self)
|
||||
|
||||
self.frame = frame
|
||||
|
||||
def tell(self):
|
||||
return self.frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(MicImageFile.format, MicImageFile, _accept)
|
||||
|
||||
Image.register_extension(MicImageFile.format, ".mic")
|
83
.venv/lib/python3.9/site-packages/PIL/MpegImagePlugin.py
Normal file
83
.venv/lib/python3.9/site-packages/PIL/MpegImagePlugin.py
Normal file
@ -0,0 +1,83 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
from ._binary import i8
|
||||
|
||||
#
|
||||
# Bitstream parser
|
||||
|
||||
|
||||
class BitStream:
|
||||
def __init__(self, fp):
|
||||
self.fp = fp
|
||||
self.bits = 0
|
||||
self.bitbuffer = 0
|
||||
|
||||
def next(self):
|
||||
return i8(self.fp.read(1))
|
||||
|
||||
def peek(self, bits):
|
||||
while self.bits < bits:
|
||||
c = self.next()
|
||||
if c < 0:
|
||||
self.bits = 0
|
||||
continue
|
||||
self.bitbuffer = (self.bitbuffer << 8) + c
|
||||
self.bits += 8
|
||||
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
|
||||
|
||||
def skip(self, bits):
|
||||
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):
|
||||
v = self.peek(bits)
|
||||
self.bits = self.bits - bits
|
||||
return v
|
||||
|
||||
|
||||
##
|
||||
# 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):
|
||||
|
||||
s = BitStream(self.fp)
|
||||
|
||||
if s.read(32) != 0x1B3:
|
||||
raise SyntaxError("not an MPEG file")
|
||||
|
||||
self.mode = "RGB"
|
||||
self._size = s.read(12), s.read(12)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
||||
Image.register_open(MpegImageFile.format, MpegImageFile)
|
||||
|
||||
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])
|
||||
|
||||
Image.register_mime(MpegImageFile.format, "video/mpeg")
|
136
.venv/lib/python3.9/site-packages/PIL/MpoImagePlugin.py
Normal file
136
.venv/lib/python3.9/site-packages/PIL/MpoImagePlugin.py
Normal file
@ -0,0 +1,136 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile, JpegImagePlugin
|
||||
from ._binary import i16be as i16
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return JpegImagePlugin._accept(prefix)
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
# Note that we can only save the current frame at present
|
||||
return JpegImagePlugin._save(im, fp, filename)
|
||||
|
||||
|
||||
##
|
||||
# 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):
|
||||
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=None):
|
||||
self.mpinfo = mpheader if mpheader is not None else self._getmp()
|
||||
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
|
||||
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):
|
||||
self.__fp.seek(pos)
|
||||
|
||||
def seek(self, frame):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.fp = self.__fp
|
||||
self.offset = self.__mpoffsets[frame]
|
||||
|
||||
self.fp.seek(self.offset + 2) # skip SOI marker
|
||||
segment = self.fp.read(2)
|
||||
if not segment:
|
||||
raise ValueError("No data found for frame")
|
||||
if i16(segment) == 0xFFE1: # APP1
|
||||
n = i16(self.fp.read(2)) - 2
|
||||
self.info["exif"] = ImageFile._safe_read(self.fp, n)
|
||||
|
||||
mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
|
||||
if mptype.startswith("Large Thumbnail"):
|
||||
exif = self.getexif().get_ifd(0x8769)
|
||||
if 40962 in exif and 40963 in exif:
|
||||
self._size = (exif[40962], exif[40963])
|
||||
elif "exif" in self.info:
|
||||
del self.info["exif"]
|
||||
|
||||
self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))]
|
||||
self.__frame = frame
|
||||
|
||||
def tell(self):
|
||||
return self.__frame
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
@staticmethod
|
||||
def adopt(jpeg_instance, mpheader=None):
|
||||
"""
|
||||
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
|
||||
jpeg_instance._after_jpeg_open(mpheader)
|
||||
return jpeg_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_extension(MpoImageFile.format, ".mpo")
|
||||
|
||||
Image.register_mime(MpoImageFile.format, "image/mpo")
|
194
.venv/lib/python3.9/site-packages/PIL/MspImagePlugin.py
Normal file
194
.venv/lib/python3.9/site-packages/PIL/MspImagePlugin.py
Normal file
@ -0,0 +1,194 @@
|
||||
#
|
||||
# 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: http://www.fileformat.info/format/mspaint/egff.htm
|
||||
|
||||
import io
|
||||
import struct
|
||||
|
||||
from . import Image, ImageFile
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import o16le as o16
|
||||
|
||||
#
|
||||
# read MSP files
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] in [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):
|
||||
|
||||
# Header
|
||||
s = self.fp.read(32)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not an MSP file")
|
||||
|
||||
# Header checksum
|
||||
checksum = 0
|
||||
for i in range(0, 32, 2):
|
||||
checksum = checksum ^ i16(s, i)
|
||||
if checksum != 0:
|
||||
raise SyntaxError("bad MSP checksum")
|
||||
|
||||
self.mode = "1"
|
||||
self._size = i16(s, 4), i16(s, 6)
|
||||
|
||||
if s[:4] == b"DanM":
|
||||
self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
|
||||
else:
|
||||
self.tile = [("MSP", (0, 0) + self.size, 32, None)]
|
||||
|
||||
|
||||
class MspDecoder(ImageFile.PyDecoder):
|
||||
# The algo for the MSP decoder is from
|
||||
# http://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):
|
||||
|
||||
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:
|
||||
raise OSError("Truncated MSP file in row map") 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:
|
||||
raise OSError(
|
||||
"Truncated MSP file, expected %d bytes on row %s", (rowlen, x)
|
||||
)
|
||||
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:
|
||||
raise OSError(f"Corrupted MSP file in row {x}") from e
|
||||
|
||||
self.set_as_raw(img.getvalue(), ("1", 0, 1))
|
||||
|
||||
return 0, 0
|
||||
|
||||
|
||||
Image.register_decoder("MSP", MspDecoder)
|
||||
|
||||
|
||||
#
|
||||
# write MSP files (uncompressed only)
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
|
||||
if im.mode != "1":
|
||||
raise OSError(f"cannot write mode {im.mode} as MSP")
|
||||
|
||||
# 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, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))])
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(MspImageFile.format, MspImageFile, _accept)
|
||||
Image.register_save(MspImageFile.format, _save)
|
||||
|
||||
Image.register_extension(MspImageFile.format, ".msp")
|
235
.venv/lib/python3.9/site-packages/PIL/PSDraw.py
Normal file
235
.venv/lib/python3.9/site-packages/PIL/PSDraw.py
Normal file
@ -0,0 +1,235 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
from . import EpsImagePlugin
|
||||
|
||||
##
|
||||
# Simple PostScript graphics interface.
|
||||
|
||||
|
||||
class PSDraw:
|
||||
"""
|
||||
Sets up printing to the given file. If ``fp`` is omitted,
|
||||
``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
|
||||
"""
|
||||
|
||||
def __init__(self, fp=None):
|
||||
if not fp:
|
||||
try:
|
||||
fp = sys.stdout.buffer
|
||||
except AttributeError:
|
||||
fp = sys.stdout
|
||||
self.fp = fp
|
||||
|
||||
def begin_document(self, id=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 = {}
|
||||
|
||||
def end_document(self):
|
||||
"""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, size):
|
||||
"""
|
||||
Selects which font to use.
|
||||
|
||||
:param font: A PostScript font name
|
||||
:param size: Size in points.
|
||||
"""
|
||||
font = bytes(font, "UTF-8")
|
||||
if font not in self.isofont:
|
||||
# reencode font
|
||||
self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font))
|
||||
self.isofont[font] = 1
|
||||
# rough
|
||||
self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font))
|
||||
|
||||
def line(self, xy0, xy1):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Draws a rectangle.
|
||||
|
||||
:param box: A 4-tuple of integers whose order and function is currently
|
||||
undocumented.
|
||||
|
||||
Hint: the tuple is passed into this format string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
%d %d M %d %d 0 Vr\n
|
||||
"""
|
||||
self.fp.write(b"%d %d M %d %d 0 Vr\n" % box)
|
||||
|
||||
def text(self, xy, text):
|
||||
"""
|
||||
Draws text at the given position. You must use
|
||||
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
|
||||
"""
|
||||
text = bytes(text, "UTF-8")
|
||||
text = b"\\(".join(text.split(b"("))
|
||||
text = b"\\)".join(text.split(b")"))
|
||||
xy += (text,)
|
||||
self.fp.write(b"%d %d M (%s) S\n" % xy)
|
||||
|
||||
def image(self, box, im, dpi=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 # greyscale
|
||||
# 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, None, 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 neg 0 exch rlineto
|
||||
exch neg 0 rlineto
|
||||
0 exch rlineto
|
||||
100 div setgray fill 0 setgray } 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
|
||||
"""
|
53
.venv/lib/python3.9/site-packages/PIL/PaletteFile.py
Normal file
53
.venv/lib/python3.9/site-packages/PIL/PaletteFile.py
Normal file
@ -0,0 +1,53 @@
|
||||
#
|
||||
# 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 ._binary import o8
|
||||
|
||||
|
||||
class PaletteFile:
|
||||
"""File handler for Teragon-style palette files."""
|
||||
|
||||
rawmode = "RGB"
|
||||
|
||||
def __init__(self, fp):
|
||||
|
||||
self.palette = [(i, i, i) for i in range(256)]
|
||||
|
||||
while True:
|
||||
|
||||
s = fp.readline()
|
||||
|
||||
if not s:
|
||||
break
|
||||
if s[0:1] == b"#":
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
||||
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:
|
||||
self.palette[i] = o8(r) + o8(g) + o8(b)
|
||||
|
||||
self.palette = b"".join(self.palette)
|
||||
|
||||
def getpalette(self):
|
||||
|
||||
return self.palette, self.rawmode
|
227
.venv/lib/python3.9/site-packages/PIL/PalmImagePlugin.py
Normal file
227
.venv/lib/python3.9/site-packages/PIL/PalmImagePlugin.py
Normal file
@ -0,0 +1,227 @@
|
||||
#
|
||||
# The Python Imaging Library.
|
||||
# $Id$
|
||||
#
|
||||
|
||||
##
|
||||
# Image plugin for Palm pixmap images (output only).
|
||||
##
|
||||
|
||||
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.new("L", (1, len(_Palm8BitColormapValues)))
|
||||
image.putdata(list(range(len(_Palm8BitColormapValues))))
|
||||
palettedata = ()
|
||||
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, fp, filename):
|
||||
|
||||
if im.mode == "P":
|
||||
|
||||
# we assume this is a color Palm image with the standard colormap,
|
||||
# unless the "info" dict has a "custom-colormap" field
|
||||
|
||||
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 greyscale from white (0) to black (1)
|
||||
bpp = im.encoderinfo["bpp"]
|
||||
im = im.point(
|
||||
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: 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"]
|
||||
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval))
|
||||
else:
|
||||
raise OSError(f"cannot write mode {im.mode} as Palm")
|
||||
|
||||
# we ignore the palette here
|
||||
im.mode = "P"
|
||||
rawmode = "P;" + str(bpp)
|
||||
version = 1
|
||||
|
||||
elif im.mode == "1":
|
||||
|
||||
# monochrome -- write it inverted, as is the Palm standard
|
||||
rawmode = "1;I"
|
||||
bpp = 1
|
||||
version = 0
|
||||
|
||||
else:
|
||||
|
||||
raise OSError(f"cannot write mode {im.mode} as Palm")
|
||||
|
||||
#
|
||||
# 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" and "custom-colormap" in im.info:
|
||||
flags = flags & _FLAGS["custom-colormap"]
|
||||
colormapsize = 4 * 256 + 2
|
||||
colormapmode = im.palette.mode
|
||||
colormap = im.getdata().getpalette()
|
||||
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 > 0:
|
||||
fp.write(o16b(256))
|
||||
for i in range(256):
|
||||
fp.write(o8(i))
|
||||
if colormapmode == "RGB":
|
||||
fp.write(
|
||||
o8(colormap[3 * i])
|
||||
+ o8(colormap[3 * i + 1])
|
||||
+ o8(colormap[3 * i + 2])
|
||||
)
|
||||
elif colormapmode == "RGBA":
|
||||
fp.write(
|
||||
o8(colormap[4 * i])
|
||||
+ o8(colormap[4 * i + 1])
|
||||
+ o8(colormap[4 * i + 2])
|
||||
)
|
||||
|
||||
# now convert data to raw form
|
||||
ImageFile._save(im, fp, [("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")
|
63
.venv/lib/python3.9/site-packages/PIL/PcdImagePlugin.py
Normal file
63
.venv/lib/python3.9/site-packages/PIL/PcdImagePlugin.py
Normal file
@ -0,0 +1,63 @@
|
||||
#
|
||||
# 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 . 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):
|
||||
|
||||
# rough
|
||||
self.fp.seek(2048)
|
||||
s = self.fp.read(2048)
|
||||
|
||||
if s[:4] != b"PCD_":
|
||||
raise SyntaxError("not a PCD file")
|
||||
|
||||
orientation = s[1538] & 3
|
||||
self.tile_post_rotate = None
|
||||
if orientation == 1:
|
||||
self.tile_post_rotate = 90
|
||||
elif orientation == 3:
|
||||
self.tile_post_rotate = -90
|
||||
|
||||
self.mode = "RGB"
|
||||
self._size = 768, 512 # FIXME: not correct for rotated images!
|
||||
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
|
||||
|
||||
def load_end(self):
|
||||
if self.tile_post_rotate:
|
||||
# Handle rotated PCDs
|
||||
self.im = self.im.rotate(self.tile_post_rotate)
|
||||
self._size = self.im.size
|
||||
|
||||
|
||||
#
|
||||
# registry
|
||||
|
||||
Image.register_open(PcdImageFile.format, PcdImageFile)
|
||||
|
||||
Image.register_extension(PcdImageFile.format, ".pcd")
|
248
.venv/lib/python3.9/site-packages/PIL/PcfFontFile.py
Normal file
248
.venv/lib/python3.9/site-packages/PIL/PcfFontFile.py
Normal file
@ -0,0 +1,248 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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 = [
|
||||
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, o):
|
||||
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, charset_encoding="iso8859-1"):
|
||||
|
||||
self.charset_encoding = charset_encoding
|
||||
|
||||
magic = l32(fp.read(4))
|
||||
if magic != PCF_MAGIC:
|
||||
raise SyntaxError("not a PCF file")
|
||||
|
||||
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 in range(256):
|
||||
ix = encoding[ch]
|
||||
if ix is not None:
|
||||
x, y, l, r, w, a, d, f = metrics[ix]
|
||||
glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix]
|
||||
self.glyph[ch] = glyph
|
||||
|
||||
def _getformat(self, tag):
|
||||
|
||||
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):
|
||||
|
||||
#
|
||||
# font properties
|
||||
|
||||
properties = {}
|
||||
|
||||
fp, format, i16, i32 = self._getformat(PCF_PROPERTIES)
|
||||
|
||||
nprops = i32(fp.read(4))
|
||||
|
||||
# read property description
|
||||
p = []
|
||||
for i in range(nprops):
|
||||
p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))))
|
||||
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:
|
||||
k = sz(data, k)
|
||||
if s:
|
||||
v = sz(data, v)
|
||||
properties[k] = v
|
||||
|
||||
return properties
|
||||
|
||||
def _load_metrics(self):
|
||||
|
||||
#
|
||||
# font metrics
|
||||
|
||||
metrics = []
|
||||
|
||||
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):
|
||||
|
||||
#
|
||||
# bitmap data
|
||||
|
||||
bitmaps = []
|
||||
|
||||
fp, format, i16, i32 = self._getformat(PCF_BITMAPS)
|
||||
|
||||
nbitmaps = i32(fp.read(4))
|
||||
|
||||
if nbitmaps != len(metrics):
|
||||
raise OSError("Wrong number of bitmaps")
|
||||
|
||||
offsets = []
|
||||
for i in range(nbitmaps):
|
||||
offsets.append(i32(fp.read(4)))
|
||||
|
||||
bitmapSizes = []
|
||||
for i in range(4):
|
||||
bitmapSizes.append(i32(fp.read(4)))
|
||||
|
||||
# byteorder = format & 4 # non-zero => MSB
|
||||
bitorder = format & 8 # non-zero => MSB
|
||||
padindex = format & 3
|
||||
|
||||
bitmapsize = bitmapSizes[padindex]
|
||||
offsets.append(bitmapsize)
|
||||
|
||||
data = fp.read(bitmapsize)
|
||||
|
||||
pad = BYTES_PER_ROW[padindex]
|
||||
mode = "1;R"
|
||||
if bitorder:
|
||||
mode = "1"
|
||||
|
||||
for i in range(nbitmaps):
|
||||
x, y, l, r, w, a, d, f = metrics[i]
|
||||
b, e = offsets[i], offsets[i + 1]
|
||||
bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x)))
|
||||
|
||||
return bitmaps
|
||||
|
||||
def _load_encoding(self):
|
||||
|
||||
# map character code to bitmap index
|
||||
encoding = [None] * 256
|
||||
|
||||
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
|
||||
|
||||
firstCol, lastCol = i16(fp.read(2)), i16(fp.read(2))
|
||||
firstRow, lastRow = i16(fp.read(2)), i16(fp.read(2))
|
||||
|
||||
i16(fp.read(2)) # default
|
||||
|
||||
nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1)
|
||||
|
||||
encodingOffsets = [i16(fp.read(2)) for _ in range(nencoding)]
|
||||
|
||||
for i in range(firstCol, len(encoding)):
|
||||
try:
|
||||
encodingOffset = encodingOffsets[
|
||||
ord(bytearray([i]).decode(self.charset_encoding))
|
||||
]
|
||||
if encodingOffset != 0xFFFF:
|
||||
encoding[i] = encodingOffset
|
||||
except UnicodeDecodeError:
|
||||
# character is not supported in selected encoding
|
||||
pass
|
||||
|
||||
return encoding
|
218
.venv/lib/python3.9/site-packages/PIL/PcxImagePlugin.py
Normal file
218
.venv/lib/python3.9/site-packages/PIL/PcxImagePlugin.py
Normal file
@ -0,0 +1,218 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
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):
|
||||
return 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):
|
||||
|
||||
# header
|
||||
s = self.fp.read(128)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a PCX file")
|
||||
|
||||
# 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]:
|
||||
raise SyntaxError("bad PCX image size")
|
||||
logger.debug("BBox: %s %s %s %s", *bbox)
|
||||
|
||||
# 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 = "P;%dL" % planes
|
||||
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 greyscale
|
||||
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:])
|
||||
self.fp.seek(128)
|
||||
|
||||
elif version == 5 and bits == 8 and planes == 3:
|
||||
mode = "RGB"
|
||||
rawmode = "RGB;L"
|
||||
|
||||
else:
|
||||
raise OSError("unknown PCX mode")
|
||||
|
||||
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 = [("pcx", bbox, self.fp.tell(), (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, fp, filename):
|
||||
|
||||
try:
|
||||
version, bits, planes, rawmode = SAVE[im.mode]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Cannot save {im.mode} images as PCX") 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, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))])
|
||||
|
||||
if im.mode == "P":
|
||||
# colour palette
|
||||
fp.write(o8(12))
|
||||
fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes
|
||||
elif im.mode == "L":
|
||||
# greyscale 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")
|
240
.venv/lib/python3.9/site-packages/PIL/PdfImagePlugin.py
Normal file
240
.venv/lib/python3.9/site-packages/PIL/PdfImagePlugin.py
Normal file
@ -0,0 +1,240 @@
|
||||
#
|
||||
# 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).
|
||||
##
|
||||
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
|
||||
from . import Image, ImageFile, ImageSequence, PdfParser, __version__
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# object ids:
|
||||
# 1. catalogue
|
||||
# 2. pages
|
||||
# 3. image
|
||||
# 4. page
|
||||
# 5. page contents
|
||||
|
||||
|
||||
def _save_all(im, fp, filename):
|
||||
_save(im, fp, filename, save_all=True)
|
||||
|
||||
|
||||
##
|
||||
# (Internal) Image save plugin for the PDF format.
|
||||
|
||||
|
||||
def _save(im, fp, filename, save_all=False):
|
||||
is_appending = im.encoderinfo.get("append", False)
|
||||
if is_appending:
|
||||
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b")
|
||||
else:
|
||||
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b")
|
||||
|
||||
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(f"created by Pillow {__version__} 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)
|
||||
numberOfPages = 0
|
||||
image_refs = []
|
||||
page_refs = []
|
||||
contents_refs = []
|
||||
for im in ims:
|
||||
im_numberOfPages = 1
|
||||
if save_all:
|
||||
try:
|
||||
im_numberOfPages = im.n_frames
|
||||
except AttributeError:
|
||||
# Image format does not have n_frames.
|
||||
# It is a single frame image
|
||||
pass
|
||||
numberOfPages += im_numberOfPages
|
||||
for i in range(im_numberOfPages):
|
||||
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()
|
||||
|
||||
pageNumber = 0
|
||||
for imSequence in ims:
|
||||
im_pages = ImageSequence.Iterator(imSequence) if save_all else [imSequence]
|
||||
for im in im_pages:
|
||||
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode
|
||||
# (packbits) or LZWDecode (tiff/lzw compression). Note that
|
||||
# PDF 1.2 also supports Flatedecode (zip compression).
|
||||
|
||||
bits = 8
|
||||
params = None
|
||||
decode = None
|
||||
|
||||
if im.mode == "1":
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
bits = 1
|
||||
elif im.mode == "L":
|
||||
filter = "DCTDecode"
|
||||
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
||||
colorspace = PdfParser.PdfName("DeviceGray")
|
||||
procset = "ImageB" # grayscale
|
||||
elif im.mode == "P":
|
||||
filter = "ASCIIHexDecode"
|
||||
palette = im.im.getpalette("RGB")
|
||||
colorspace = [
|
||||
PdfParser.PdfName("Indexed"),
|
||||
PdfParser.PdfName("DeviceRGB"),
|
||||
255,
|
||||
PdfParser.PdfBinary(palette),
|
||||
]
|
||||
procset = "ImageI" # indexed color
|
||||
elif im.mode == "RGB":
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceRGB")
|
||||
procset = "ImageC" # color images
|
||||
elif im.mode == "CMYK":
|
||||
filter = "DCTDecode"
|
||||
colorspace = PdfParser.PdfName("DeviceCMYK")
|
||||
procset = "ImageC" # color images
|
||||
decode = [1, 0, 1, 0, 1, 0, 1, 0]
|
||||
else:
|
||||
raise ValueError(f"cannot save mode {im.mode}")
|
||||
|
||||
#
|
||||
# image
|
||||
|
||||
op = io.BytesIO()
|
||||
|
||||
if filter == "ASCIIHexDecode":
|
||||
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
|
||||
elif filter == "DCTDecode":
|
||||
Image.SAVE["JPEG"](im, op, filename)
|
||||
elif filter == "FlateDecode":
|
||||
ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)])
|
||||
elif filter == "RunLengthDecode":
|
||||
ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)])
|
||||
else:
|
||||
raise ValueError(f"unsupported PDF filter ({filter})")
|
||||
|
||||
#
|
||||
# Get image characteristics
|
||||
|
||||
width, height = im.size
|
||||
|
||||
existing_pdf.write_obj(
|
||||
image_refs[pageNumber],
|
||||
stream=op.getvalue(),
|
||||
Type=PdfParser.PdfName("XObject"),
|
||||
Subtype=PdfParser.PdfName("Image"),
|
||||
Width=width, # * 72.0 / resolution,
|
||||
Height=height, # * 72.0 / resolution,
|
||||
Filter=PdfParser.PdfName(filter),
|
||||
BitsPerComponent=bits,
|
||||
Decode=decode,
|
||||
DecodeParams=params,
|
||||
ColorSpace=colorspace,
|
||||
)
|
||||
|
||||
#
|
||||
# page
|
||||
|
||||
existing_pdf.write_page(
|
||||
page_refs[pageNumber],
|
||||
Resources=PdfParser.PdfDict(
|
||||
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
|
||||
XObject=PdfParser.PdfDict(image=image_refs[pageNumber]),
|
||||
),
|
||||
MediaBox=[
|
||||
0,
|
||||
0,
|
||||
width * 72.0 / resolution,
|
||||
height * 72.0 / resolution,
|
||||
],
|
||||
Contents=contents_refs[pageNumber],
|
||||
)
|
||||
|
||||
#
|
||||
# page contents
|
||||
|
||||
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
|
||||
width * 72.0 / resolution,
|
||||
height * 72.0 / resolution,
|
||||
)
|
||||
|
||||
existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents)
|
||||
|
||||
pageNumber += 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")
|
997
.venv/lib/python3.9/site-packages/PIL/PdfParser.py
Normal file
997
.venv/lib/python3.9/site-packages/PIL/PdfParser.py
Normal file
@ -0,0 +1,997 @@
|
||||
import calendar
|
||||
import codecs
|
||||
import collections
|
||||
import mmap
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import zlib
|
||||
|
||||
|
||||
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
||||
# on page 656
|
||||
def encode_text(s):
|
||||
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):
|
||||
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, error_message):
|
||||
if not condition:
|
||||
raise PdfFormatError(error_message)
|
||||
|
||||
|
||||
class IndirectReference(
|
||||
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
|
||||
):
|
||||
def __str__(self):
|
||||
return "%s %s R" % self
|
||||
|
||||
def __bytes__(self):
|
||||
return self.__str__().encode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
other.__class__ is self.__class__
|
||||
and other.object_id == self.object_id
|
||||
and other.generation == self.generation
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.object_id, self.generation))
|
||||
|
||||
|
||||
class IndirectObjectDef(IndirectReference):
|
||||
def __str__(self):
|
||||
return "%s %s obj" % self
|
||||
|
||||
|
||||
class XrefTable:
|
||||
def __init__(self):
|
||||
self.existing_entries = {} # object ID => (offset, generation)
|
||||
self.new_entries = {} # object ID => (offset, generation)
|
||||
self.deleted_entries = {0: 65536} # object ID => generation
|
||||
self.reading_finished = False
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
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):
|
||||
try:
|
||||
return self.new_entries[key]
|
||||
except KeyError:
|
||||
return self.existing_entries[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
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:
|
||||
raise IndexError(
|
||||
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
|
||||
)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.existing_entries or key in self.new_entries
|
||||
|
||||
def __len__(self):
|
||||
return len(
|
||||
set(self.existing_entries.keys())
|
||||
| set(self.new_entries.keys())
|
||||
| set(self.deleted_entries.keys())
|
||||
)
|
||||
|
||||
def keys(self):
|
||||
return (
|
||||
set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
|
||||
) | set(self.new_entries.keys())
|
||||
|
||||
def write(self, f):
|
||||
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 = 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 = None
|
||||
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:
|
||||
def __init__(self, name):
|
||||
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):
|
||||
return self.name.decode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, PdfName) and other.name == self.name
|
||||
) or other == self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return f"PdfName({repr(self.name)})"
|
||||
|
||||
@classmethod
|
||||
def from_pdf_stream(cls, data):
|
||||
return cls(PdfParser.interpret_name(data))
|
||||
|
||||
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
|
||||
|
||||
def __bytes__(self):
|
||||
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):
|
||||
def __bytes__(self):
|
||||
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
||||
|
||||
|
||||
class PdfDict(collections.UserDict):
|
||||
def __setattr__(self, key, value):
|
||||
if key == "data":
|
||||
collections.UserDict.__setattr__(self, key, value)
|
||||
else:
|
||||
self[key.encode("us-ascii")] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
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):
|
||||
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):
|
||||
self.data = data
|
||||
|
||||
def __bytes__(self):
|
||||
return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
|
||||
|
||||
|
||||
class PdfStream:
|
||||
def __init__(self, dictionary, buf):
|
||||
self.dictionary = dictionary
|
||||
self.buf = buf
|
||||
|
||||
def decode(self):
|
||||
try:
|
||||
filter = self.dictionary.Filter
|
||||
except AttributeError:
|
||||
return self.buf
|
||||
if filter == b"FlateDecode":
|
||||
try:
|
||||
expected_length = self.dictionary.DL
|
||||
except AttributeError:
|
||||
expected_length = self.dictionary.Length
|
||||
return zlib.decompress(self.buf, bufsize=int(expected_length))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
|
||||
)
|
||||
|
||||
|
||||
def pdf_repr(x):
|
||||
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):
|
||||
return str(x).encode("us-ascii")
|
||||
elif isinstance(x, 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=None, f=None, buf=None, start_offset=0, mode="rb"):
|
||||
if buf and f:
|
||||
raise RuntimeError("specify buf or f or filename, but not both buf and f")
|
||||
self.filename = filename
|
||||
self.buf = 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 = 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 = {}
|
||||
if 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 = {}
|
||||
self.pages = []
|
||||
self.orig_pages = []
|
||||
self.pages_ref = None
|
||||
self.last_xref_section_offset = None
|
||||
self.trailer_dict = {}
|
||||
self.xref_table = XrefTable()
|
||||
self.xref_table.reading_finished = True
|
||||
if f:
|
||||
self.seek_end()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
return False # do not suppress exceptions
|
||||
|
||||
def start_writing(self):
|
||||
self.close_buf()
|
||||
self.seek_end()
|
||||
|
||||
def close_buf(self):
|
||||
try:
|
||||
self.buf.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
self.buf = None
|
||||
|
||||
def close(self):
|
||||
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):
|
||||
self.f.seek(0, os.SEEK_END)
|
||||
|
||||
def write_header(self):
|
||||
self.f.write(b"%PDF-1.4\n")
|
||||
|
||||
def write_comment(self, s):
|
||||
self.f.write(f"% {s}\n".encode("utf-8"))
|
||||
|
||||
def write_catalog(self):
|
||||
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):
|
||||
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=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 = {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, *objs, **dict_obj):
|
||||
if isinstance(ref, int):
|
||||
ref = self.pages[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(ref, *objs, **dict_obj)
|
||||
|
||||
def write_obj(self, ref, *objs, **dict_obj):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
self.file_size_total = len(self.buf)
|
||||
self.file_size_this = self.file_size_total - self.start_offset
|
||||
self.read_trailer()
|
||||
self.root_ref = self.trailer_dict[b"Root"]
|
||||
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(b"Pages" in self.root, "/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"]
|
||||
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=None):
|
||||
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 = br"[][()<>{}/%]"
|
||||
delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]"
|
||||
whitespace = br"[\000\011\012\014\015\040]"
|
||||
whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]"
|
||||
whitespace_optional = whitespace + b"*"
|
||||
whitespace_mandatory = whitespace + b"+"
|
||||
whitespace_optional_no_nl = br"[\000\011\014\015\040]*" # no "\012" aka "\n"
|
||||
newline_only = br"[\r\n]+"
|
||||
newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
|
||||
re_trailer_end = re.compile(
|
||||
whitespace_mandatory
|
||||
+ br"trailer"
|
||||
+ whitespace_optional
|
||||
+ br"\<\<(.*\>\>)"
|
||||
+ newline
|
||||
+ br"startxref"
|
||||
+ newline
|
||||
+ br"([0-9]+)"
|
||||
+ newline
|
||||
+ br"%%EOF"
|
||||
+ whitespace_optional
|
||||
+ br"$",
|
||||
re.DOTALL,
|
||||
)
|
||||
re_trailer_prev = re.compile(
|
||||
whitespace_optional
|
||||
+ br"trailer"
|
||||
+ whitespace_optional
|
||||
+ br"\<\<(.*?\>\>)"
|
||||
+ newline
|
||||
+ br"startxref"
|
||||
+ newline
|
||||
+ br"([0-9]+)"
|
||||
+ newline
|
||||
+ br"%%EOF"
|
||||
+ whitespace_optional,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def read_trailer(self):
|
||||
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, "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
|
||||
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):
|
||||
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, "previous trailer not found")
|
||||
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:
|
||||
self.read_prev_trailer(trailer_dict[b"Prev"])
|
||||
|
||||
re_whitespace_optional = re.compile(whitespace_optional)
|
||||
re_name = re.compile(
|
||||
whitespace_optional
|
||||
+ br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
)
|
||||
re_dict_start = re.compile(whitespace_optional + br"\<\<")
|
||||
re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional)
|
||||
|
||||
@classmethod
|
||||
def interpret_trailer(cls, trailer_data):
|
||||
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 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))
|
||||
value, offset = cls.get_value(trailer_data, m.end())
|
||||
trailer[key] = value
|
||||
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(br"([^#]*)(#([0-9a-fA-F]{2}))?")
|
||||
|
||||
@classmethod
|
||||
def interpret_name(cls, raw, as_text=False):
|
||||
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 + br"null(?=" + delimiter_or_ws + br")")
|
||||
re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")")
|
||||
re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")")
|
||||
re_int = re.compile(
|
||||
whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")"
|
||||
)
|
||||
re_real = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
)
|
||||
re_array_start = re.compile(whitespace_optional + br"\[")
|
||||
re_array_end = re.compile(whitespace_optional + br"]")
|
||||
re_string_hex = re.compile(
|
||||
whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>"
|
||||
)
|
||||
re_string_lit = re.compile(whitespace_optional + br"\(")
|
||||
re_indirect_reference = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"R(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
)
|
||||
re_indirect_def_start = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([-+]?[0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"obj(?="
|
||||
+ delimiter_or_ws
|
||||
+ br")"
|
||||
)
|
||||
re_indirect_def_end = re.compile(
|
||||
whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")"
|
||||
)
|
||||
re_comment = re.compile(
|
||||
br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*"
|
||||
)
|
||||
re_stream_start = re.compile(whitespace_optional + br"stream\r?\n")
|
||||
re_stream_end = re.compile(
|
||||
whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
|
||||
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, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
|
||||
if offset is None:
|
||||
return object, None
|
||||
m = cls.re_indirect_def_end.match(data, offset)
|
||||
check_format_condition(m, "indirect object definition end not found")
|
||||
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 = {}
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
while not m:
|
||||
key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
if offset is None:
|
||||
return result, None
|
||||
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
result[key] = value
|
||||
if offset is None:
|
||||
return result, None
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
offset = m.end()
|
||||
m = cls.re_stream_start.match(data, offset)
|
||||
if m:
|
||||
try:
|
||||
stream_len = int(result[b"Length"])
|
||||
except (TypeError, KeyError, ValueError) as e:
|
||||
raise PdfFormatError(
|
||||
"bad or missing Length in stream dict (%r)"
|
||||
% result.get(b"Length", None)
|
||||
) from e
|
||||
stream_data = data[m.end() : m.end() + stream_len]
|
||||
m = cls.re_stream_end.match(data, m.end() + stream_len)
|
||||
check_format_condition(m, "stream end not found")
|
||||
offset = m.end()
|
||||
result = PdfStream(PdfDict(result), stream_data)
|
||||
else:
|
||||
result = PdfDict(result)
|
||||
return result, offset
|
||||
m = cls.re_array_start.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
result = []
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
while not m:
|
||||
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
result.append(value)
|
||||
if offset is None:
|
||||
return result, None
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
return result, 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)
|
||||
raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32]))
|
||||
|
||||
re_lit_str_token = re.compile(
|
||||
br"(\\[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, offset):
|
||||
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()
|
||||
raise PdfFormatError("unfinished literal string")
|
||||
|
||||
re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline)
|
||||
re_xref_subsection_start = re.compile(
|
||||
whitespace_optional
|
||||
+ br"([0-9]+)"
|
||||
+ whitespace_mandatory
|
||||
+ br"([0-9]+)"
|
||||
+ whitespace_optional
|
||||
+ newline_only
|
||||
)
|
||||
re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
|
||||
|
||||
def read_xref_table(self, xref_section_offset):
|
||||
subsection_found = False
|
||||
m = self.re_xref_section_start.match(
|
||||
self.buf, xref_section_offset + self.start_offset
|
||||
)
|
||||
check_format_condition(m, "xref section start not found")
|
||||
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, "xref entry not found")
|
||||
offset = m.end()
|
||||
is_free = m.group(3) == b"f"
|
||||
generation = int(m.group(2))
|
||||
if not is_free:
|
||||
new_entry = (int(m.group(1)), generation)
|
||||
check_format_condition(
|
||||
i not in self.xref_table or self.xref_table[i] == new_entry,
|
||||
"xref entry duplicated (and not identical)",
|
||||
)
|
||||
self.xref_table[i] = new_entry
|
||||
return offset
|
||||
|
||||
def read_indirect(self, ref, max_nesting=-1):
|
||||
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}",
|
||||
)
|
||||
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=None):
|
||||
if node is None:
|
||||
node = self.page_tree_root
|
||||
check_format_condition(
|
||||
node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
|
||||
)
|
||||
pages = []
|
||||
for kid in 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
|
70
.venv/lib/python3.9/site-packages/PIL/PixarImagePlugin.py
Normal file
70
.venv/lib/python3.9/site-packages/PIL/PixarImagePlugin.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
from ._binary import i16le as i16
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[:4] == 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):
|
||||
|
||||
# assuming a 4-byte magic label
|
||||
s = self.fp.read(4)
|
||||
if not _accept(s):
|
||||
raise SyntaxError("not a PIXAR file")
|
||||
|
||||
# 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 = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))]
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
Image.register_open(PixarImageFile.format, PixarImageFile, _accept)
|
||||
|
||||
Image.register_extension(PixarImageFile.format, ".pxr")
|
1407
.venv/lib/python3.9/site-packages/PIL/PngImagePlugin.py
Normal file
1407
.venv/lib/python3.9/site-packages/PIL/PngImagePlugin.py
Normal file
File diff suppressed because it is too large
Load Diff
164
.venv/lib/python3.9/site-packages/PIL/PpmImagePlugin.py
Normal file
164
.venv/lib/python3.9/site-packages/PIL/PpmImagePlugin.py
Normal file
@ -0,0 +1,164 @@
|
||||
#
|
||||
# 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 . import Image, ImageFile
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d"
|
||||
|
||||
MODES = {
|
||||
# standard
|
||||
b"P4": "1",
|
||||
b"P5": "L",
|
||||
b"P6": "RGB",
|
||||
# extensions
|
||||
b"P0CMYK": "CMYK",
|
||||
# PIL extensions (for test purposes only)
|
||||
b"PyP": "P",
|
||||
b"PyRGBA": "RGBA",
|
||||
b"PyCMYK": "CMYK",
|
||||
}
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
return prefix[0:1] == b"P" and prefix[1] in b"0456y"
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for PBM, PGM, and PPM images.
|
||||
|
||||
|
||||
class PpmImageFile(ImageFile.ImageFile):
|
||||
|
||||
format = "PPM"
|
||||
format_description = "Pbmplus image"
|
||||
|
||||
def _token(self, s=b""):
|
||||
while True: # read until next whitespace
|
||||
c = self.fp.read(1)
|
||||
if not c or c in b_whitespace:
|
||||
break
|
||||
if c > b"\x79":
|
||||
raise ValueError("Expected ASCII value, found binary")
|
||||
s = s + c
|
||||
if len(s) > 9:
|
||||
raise ValueError("Expected int, got > 9 digits")
|
||||
return s
|
||||
|
||||
def _open(self):
|
||||
|
||||
# check magic
|
||||
s = self.fp.read(1)
|
||||
if s != b"P":
|
||||
raise SyntaxError("not a PPM file")
|
||||
magic_number = self._token(s)
|
||||
mode = MODES[magic_number]
|
||||
|
||||
self.custom_mimetype = {
|
||||
b"P4": "image/x-portable-bitmap",
|
||||
b"P5": "image/x-portable-graymap",
|
||||
b"P6": "image/x-portable-pixmap",
|
||||
}.get(magic_number)
|
||||
|
||||
if mode == "1":
|
||||
self.mode = "1"
|
||||
rawmode = "1;I"
|
||||
else:
|
||||
self.mode = rawmode = mode
|
||||
|
||||
for ix in range(3):
|
||||
while True:
|
||||
while True:
|
||||
s = self.fp.read(1)
|
||||
if s not in b_whitespace:
|
||||
break
|
||||
if s == b"":
|
||||
raise ValueError("File does not extend beyond magic number")
|
||||
if s != b"#":
|
||||
break
|
||||
s = self.fp.readline()
|
||||
s = int(self._token(s))
|
||||
if ix == 0:
|
||||
xsize = s
|
||||
elif ix == 1:
|
||||
ysize = s
|
||||
if mode == "1":
|
||||
break
|
||||
elif ix == 2:
|
||||
# maxgrey
|
||||
if s > 255:
|
||||
if not mode == "L":
|
||||
raise ValueError(f"Too many colors for band: {s}")
|
||||
if s < 2 ** 16:
|
||||
self.mode = "I"
|
||||
rawmode = "I;16B"
|
||||
else:
|
||||
self.mode = "I"
|
||||
rawmode = "I;32B"
|
||||
|
||||
self._size = xsize, ysize
|
||||
self.tile = [("raw", (0, 0, xsize, ysize), self.fp.tell(), (rawmode, 0, 1))]
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _save(im, fp, filename):
|
||||
if im.mode == "1":
|
||||
rawmode, head = "1;I", b"P4"
|
||||
elif im.mode == "L":
|
||||
rawmode, head = "L", b"P5"
|
||||
elif im.mode == "I":
|
||||
if im.getextrema()[1] < 2 ** 16:
|
||||
rawmode, head = "I;16B", b"P5"
|
||||
else:
|
||||
rawmode, head = "I;32B", b"P5"
|
||||
elif im.mode == "RGB":
|
||||
rawmode, head = "RGB", b"P6"
|
||||
elif im.mode == "RGBA":
|
||||
rawmode, head = "RGB", b"P6"
|
||||
else:
|
||||
raise OSError(f"cannot write mode {im.mode} as PPM")
|
||||
fp.write(head + ("\n%d %d\n" % im.size).encode("ascii"))
|
||||
if head == b"P6":
|
||||
fp.write(b"255\n")
|
||||
if head == b"P5":
|
||||
if rawmode == "L":
|
||||
fp.write(b"255\n")
|
||||
elif rawmode == "I;16B":
|
||||
fp.write(b"65535\n")
|
||||
elif rawmode == "I;32B":
|
||||
fp.write(b"2147483648\n")
|
||||
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
|
||||
|
||||
# ALTERNATIVE: save via builtin debug function
|
||||
# im._dump(filename)
|
||||
|
||||
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
||||
Image.register_save(PpmImageFile.format, _save)
|
||||
|
||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
|
||||
|
||||
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
|
324
.venv/lib/python3.9/site-packages/PIL/PsdImagePlugin.py
Normal file
324
.venv/lib/python3.9/site-packages/PIL/PsdImagePlugin.py
Normal file
@ -0,0 +1,324 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import io
|
||||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
|
||||
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):
|
||||
return prefix[:4] == 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):
|
||||
|
||||
read = self.fp.read
|
||||
|
||||
#
|
||||
# header
|
||||
|
||||
s = read(26)
|
||||
if not _accept(s) or i16(s, 4) != 1:
|
||||
raise SyntaxError("not a PSD file")
|
||||
|
||||
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:
|
||||
raise OSError("not enough channels")
|
||||
|
||||
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 = []
|
||||
|
||||
size = i32(read(4))
|
||||
if size:
|
||||
end = self.fp.tell() + size
|
||||
size = i32(read(4))
|
||||
if size:
|
||||
_layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size))
|
||||
self.layers = _layerinfo(_layer_data, size)
|
||||
self.fp.seek(end)
|
||||
self.n_frames = len(self.layers)
|
||||
self.is_animated = self.n_frames > 1
|
||||
|
||||
#
|
||||
# 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
|
||||
|
||||
def seek(self, layer):
|
||||
if not self._seek_check(layer):
|
||||
return
|
||||
|
||||
# seek to given layer (1..max)
|
||||
try:
|
||||
name, mode, bbox, tile = self.layers[layer - 1]
|
||||
self.mode = mode
|
||||
self.tile = tile
|
||||
self.frame = layer
|
||||
self.fp = self.__fp
|
||||
return name, bbox
|
||||
except IndexError as e:
|
||||
raise EOFError("no such layer") from e
|
||||
|
||||
def tell(self):
|
||||
# return layer number (0=image, 1..max=layers)
|
||||
return self.frame
|
||||
|
||||
def load_prepare(self):
|
||||
# create image memory if necessary
|
||||
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
|
||||
self.im = Image.core.fill(self.mode, self.size, 0)
|
||||
# create palette (optional)
|
||||
if self.mode == "P":
|
||||
Image.Image.load(self)
|
||||
|
||||
def _close__fp(self):
|
||||
try:
|
||||
if self.__fp != self.fp:
|
||||
self.__fp.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
self.__fp = None
|
||||
|
||||
|
||||
def _layerinfo(fp, ct_bytes):
|
||||
# read layerinfo block
|
||||
layers = []
|
||||
|
||||
def read(size):
|
||||
return ImageFile._safe_read(fp, size)
|
||||
|
||||
ct = i16(read(2))
|
||||
|
||||
# sanity check
|
||||
if ct_bytes < (abs(ct) * 20):
|
||||
raise SyntaxError("Layer block too short for number of layers requested")
|
||||
|
||||
for i in range(abs(ct)):
|
||||
|
||||
# bounding box
|
||||
y0 = i32(read(4))
|
||||
x0 = i32(read(4))
|
||||
y1 = i32(read(4))
|
||||
x1 = i32(read(4))
|
||||
|
||||
# image info
|
||||
info = []
|
||||
mode = []
|
||||
ct_types = i16(read(2))
|
||||
types = list(range(ct_types))
|
||||
if len(types) > 4:
|
||||
continue
|
||||
|
||||
for i in types:
|
||||
type = i16(read(2))
|
||||
|
||||
if type == 65535:
|
||||
m = "A"
|
||||
else:
|
||||
m = "RGBA"[type]
|
||||
|
||||
mode.append(m)
|
||||
size = i32(read(4))
|
||||
info.append((m, size))
|
||||
|
||||
# figure out the image mode
|
||||
mode.sort()
|
||||
if mode == ["R"]:
|
||||
mode = "L"
|
||||
elif mode == ["B", "G", "R"]:
|
||||
mode = "RGB"
|
||||
elif mode == ["A", "B", "G", "R"]:
|
||||
mode = "RGBA"
|
||||
else:
|
||||
mode = None # unknown
|
||||
|
||||
# skip over blend flags and extra information
|
||||
read(12) # filler
|
||||
name = ""
|
||||
size = i32(read(4)) # length of the extra data field
|
||||
combined = 0
|
||||
if size:
|
||||
data_end = fp.tell() + size
|
||||
|
||||
length = i32(read(4))
|
||||
if length:
|
||||
fp.seek(length - 16, io.SEEK_CUR)
|
||||
combined += length + 4
|
||||
|
||||
length = i32(read(4))
|
||||
if length:
|
||||
fp.seek(length, io.SEEK_CUR)
|
||||
combined += length + 4
|
||||
|
||||
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")
|
||||
combined += length + 1
|
||||
|
||||
fp.seek(data_end)
|
||||
layers.append((name, mode, (x0, y0, x1, y1)))
|
||||
|
||||
# get tiles
|
||||
i = 0
|
||||
for name, mode, bbox in layers:
|
||||
tile = []
|
||||
for m in mode:
|
||||
t = _maketile(fp, m, bbox, 1)
|
||||
if t:
|
||||
tile.extend(t)
|
||||
layers[i] = name, mode, bbox, tile
|
||||
i += 1
|
||||
|
||||
return layers
|
||||
|
||||
|
||||
def _maketile(file, mode, bbox, channels):
|
||||
|
||||
tile = None
|
||||
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
|
||||
tile = []
|
||||
for channel in range(channels):
|
||||
layer = mode[channel]
|
||||
if mode == "CMYK":
|
||||
layer += ";I"
|
||||
tile.append(("raw", bbox, offset, layer))
|
||||
offset = offset + xsize * ysize
|
||||
|
||||
elif compression == 1:
|
||||
#
|
||||
# packbits compression
|
||||
i = 0
|
||||
tile = []
|
||||
bytecount = read(channels * ysize * 2)
|
||||
offset = file.tell()
|
||||
for channel in range(channels):
|
||||
layer = mode[channel]
|
||||
if mode == "CMYK":
|
||||
layer += ";I"
|
||||
tile.append(("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 tile
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# registry
|
||||
|
||||
|
||||
Image.register_open(PsdImageFile.format, PsdImageFile, _accept)
|
||||
|
||||
Image.register_extension(PsdImageFile.format, ".psd")
|
||||
|
||||
Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop")
|
353
.venv/lib/python3.9/site-packages/PIL/PyAccess.py
Normal file
353
.venv/lib/python3.9/site-packages/PIL/PyAccess.py
Normal file
@ -0,0 +1,353 @@
|
||||
#
|
||||
# The Python Imaging Library
|
||||
# Pillow fork
|
||||
#
|
||||
# Python implementation of the PixelAccess Object
|
||||
#
|
||||
# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved.
|
||||
# Copyright (c) 1995-2009 by Fredrik Lundh.
|
||||
# Copyright (c) 2013 Eric Soroos
|
||||
#
|
||||
# See the README file for information on usage and redistribution
|
||||
#
|
||||
|
||||
# Notes:
|
||||
#
|
||||
# * Implements the pixel access object following Access.
|
||||
# * Does not implement the line functions, as they don't appear to be used
|
||||
# * Taking only the tuple form, which is used from python.
|
||||
# * Fill.c uses the integer form, but it's still going to use the old
|
||||
# Access.c implementation.
|
||||
#
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
try:
|
||||
from cffi import FFI
|
||||
|
||||
defs = """
|
||||
struct Pixel_RGBA {
|
||||
unsigned char r,g,b,a;
|
||||
};
|
||||
struct Pixel_I16 {
|
||||
unsigned char l,r;
|
||||
};
|
||||
"""
|
||||
ffi = FFI()
|
||||
ffi.cdef(defs)
|
||||
except ImportError as ex:
|
||||
# Allow error import for doc purposes, but error out when accessing
|
||||
# anything in core.
|
||||
from ._util import deferred_error
|
||||
|
||||
FFI = ffi = deferred_error(ex)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PyAccess:
|
||||
def __init__(self, img, readonly=False):
|
||||
vals = dict(img.im.unsafe_ptrs)
|
||||
self.readonly = readonly
|
||||
self.image8 = ffi.cast("unsigned char **", vals["image8"])
|
||||
self.image32 = ffi.cast("int **", vals["image32"])
|
||||
self.image = ffi.cast("unsigned char **", vals["image"])
|
||||
self.xsize, self.ysize = img.im.size
|
||||
self._img = img
|
||||
|
||||
# Keep pointer to im object to prevent dereferencing.
|
||||
self._im = img.im
|
||||
if self._im.mode == "P":
|
||||
self._palette = img.palette
|
||||
|
||||
# Debugging is polluting test traces, only useful here
|
||||
# when hacking on PyAccess
|
||||
# logger.debug("%s", vals)
|
||||
self._post_init()
|
||||
|
||||
def _post_init(self):
|
||||
pass
|
||||
|
||||
def __setitem__(self, xy, color):
|
||||
"""
|
||||
Modifies the pixel at x,y. The color is given as a single
|
||||
numerical value for single band images, and a tuple for
|
||||
multi-band images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y). See
|
||||
:ref:`coordinate-system`.
|
||||
:param color: The pixel value.
|
||||
"""
|
||||
if self.readonly:
|
||||
raise ValueError("Attempt to putpixel a read only image")
|
||||
(x, y) = xy
|
||||
if x < 0:
|
||||
x = self.xsize + x
|
||||
if y < 0:
|
||||
y = self.ysize + y
|
||||
(x, y) = self.check_xy((x, y))
|
||||
|
||||
if (
|
||||
self._im.mode == "P"
|
||||
and isinstance(color, (list, tuple))
|
||||
and len(color) in [3, 4]
|
||||
):
|
||||
# RGB or RGBA value for a P image
|
||||
color = self._palette.getcolor(color, self._img)
|
||||
|
||||
return self.set_pixel(x, y, color)
|
||||
|
||||
def __getitem__(self, xy):
|
||||
"""
|
||||
Returns the pixel at x,y. The pixel is returned as a single
|
||||
value for single band images or a tuple for multiple band
|
||||
images
|
||||
|
||||
:param xy: The pixel coordinate, given as (x, y). See
|
||||
:ref:`coordinate-system`.
|
||||
:returns: a pixel value for single band images, a tuple of
|
||||
pixel values for multiband images.
|
||||
"""
|
||||
(x, y) = xy
|
||||
if x < 0:
|
||||
x = self.xsize + x
|
||||
if y < 0:
|
||||
y = self.ysize + y
|
||||
(x, y) = self.check_xy((x, y))
|
||||
return self.get_pixel(x, y)
|
||||
|
||||
putpixel = __setitem__
|
||||
getpixel = __getitem__
|
||||
|
||||
def check_xy(self, xy):
|
||||
(x, y) = xy
|
||||
if not (0 <= x < self.xsize and 0 <= y < self.ysize):
|
||||
raise ValueError("pixel location out of range")
|
||||
return xy
|
||||
|
||||
|
||||
class _PyAccess32_2(PyAccess):
|
||||
""" PA, LA, stored in first and last bytes of a 32 bit word """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
pixel = self.pixels[y][x]
|
||||
return (pixel.r, pixel.a)
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
pixel = self.pixels[y][x]
|
||||
# tuple
|
||||
pixel.r = min(color[0], 255)
|
||||
pixel.a = min(color[1], 255)
|
||||
|
||||
|
||||
class _PyAccess32_3(PyAccess):
|
||||
""" RGB and friends, stored in the first three bytes of a 32 bit word """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
pixel = self.pixels[y][x]
|
||||
return (pixel.r, pixel.g, pixel.b)
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
pixel = self.pixels[y][x]
|
||||
# tuple
|
||||
pixel.r = min(color[0], 255)
|
||||
pixel.g = min(color[1], 255)
|
||||
pixel.b = min(color[2], 255)
|
||||
pixel.a = 255
|
||||
|
||||
|
||||
class _PyAccess32_4(PyAccess):
|
||||
""" RGBA etc, all 4 bytes of a 32 bit word """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
pixel = self.pixels[y][x]
|
||||
return (pixel.r, pixel.g, pixel.b, pixel.a)
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
pixel = self.pixels[y][x]
|
||||
# tuple
|
||||
pixel.r = min(color[0], 255)
|
||||
pixel.g = min(color[1], 255)
|
||||
pixel.b = min(color[2], 255)
|
||||
pixel.a = min(color[3], 255)
|
||||
|
||||
|
||||
class _PyAccess8(PyAccess):
|
||||
""" 1, L, P, 8 bit images stored as uint8 """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image8
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
return self.pixels[y][x]
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
try:
|
||||
# integer
|
||||
self.pixels[y][x] = min(color, 255)
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = min(color[0], 255)
|
||||
|
||||
|
||||
class _PyAccessI16_N(PyAccess):
|
||||
""" I;16 access, native bitendian without conversion """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("unsigned short **", self.image)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
return self.pixels[y][x]
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
try:
|
||||
# integer
|
||||
self.pixels[y][x] = min(color, 65535)
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = min(color[0], 65535)
|
||||
|
||||
|
||||
class _PyAccessI16_L(PyAccess):
|
||||
""" I;16L access, with conversion """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
pixel = self.pixels[y][x]
|
||||
return pixel.l + pixel.r * 256
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
pixel = self.pixels[y][x]
|
||||
try:
|
||||
color = min(color, 65535)
|
||||
except TypeError:
|
||||
color = min(color[0], 65535)
|
||||
|
||||
pixel.l = color & 0xFF # noqa: E741
|
||||
pixel.r = color >> 8
|
||||
|
||||
|
||||
class _PyAccessI16_B(PyAccess):
|
||||
""" I;16B access, with conversion """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
pixel = self.pixels[y][x]
|
||||
return pixel.l * 256 + pixel.r
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
pixel = self.pixels[y][x]
|
||||
try:
|
||||
color = min(color, 65535)
|
||||
except Exception:
|
||||
color = min(color[0], 65535)
|
||||
|
||||
pixel.l = color >> 8 # noqa: E741
|
||||
pixel.r = color & 0xFF
|
||||
|
||||
|
||||
class _PyAccessI32_N(PyAccess):
|
||||
""" Signed Int32 access, native endian """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image32
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
return self.pixels[y][x]
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
self.pixels[y][x] = color
|
||||
|
||||
|
||||
class _PyAccessI32_Swap(PyAccess):
|
||||
""" I;32L/B access, with byteswapping conversion """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = self.image32
|
||||
|
||||
def reverse(self, i):
|
||||
orig = ffi.new("int *", i)
|
||||
chars = ffi.cast("unsigned char *", orig)
|
||||
chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], chars[1], chars[0]
|
||||
return ffi.cast("int *", chars)[0]
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
return self.reverse(self.pixels[y][x])
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
self.pixels[y][x] = self.reverse(color)
|
||||
|
||||
|
||||
class _PyAccessF(PyAccess):
|
||||
""" 32 bit float access """
|
||||
|
||||
def _post_init(self, *args, **kwargs):
|
||||
self.pixels = ffi.cast("float **", self.image32)
|
||||
|
||||
def get_pixel(self, x, y):
|
||||
return self.pixels[y][x]
|
||||
|
||||
def set_pixel(self, x, y, color):
|
||||
try:
|
||||
# not a tuple
|
||||
self.pixels[y][x] = color
|
||||
except TypeError:
|
||||
# tuple
|
||||
self.pixels[y][x] = color[0]
|
||||
|
||||
|
||||
mode_map = {
|
||||
"1": _PyAccess8,
|
||||
"L": _PyAccess8,
|
||||
"P": _PyAccess8,
|
||||
"LA": _PyAccess32_2,
|
||||
"La": _PyAccess32_2,
|
||||
"PA": _PyAccess32_2,
|
||||
"RGB": _PyAccess32_3,
|
||||
"LAB": _PyAccess32_3,
|
||||
"HSV": _PyAccess32_3,
|
||||
"YCbCr": _PyAccess32_3,
|
||||
"RGBA": _PyAccess32_4,
|
||||
"RGBa": _PyAccess32_4,
|
||||
"RGBX": _PyAccess32_4,
|
||||
"CMYK": _PyAccess32_4,
|
||||
"F": _PyAccessF,
|
||||
"I": _PyAccessI32_N,
|
||||
}
|
||||
|
||||
if sys.byteorder == "little":
|
||||
mode_map["I;16"] = _PyAccessI16_N
|
||||
mode_map["I;16L"] = _PyAccessI16_N
|
||||
mode_map["I;16B"] = _PyAccessI16_B
|
||||
|
||||
mode_map["I;32L"] = _PyAccessI32_N
|
||||
mode_map["I;32B"] = _PyAccessI32_Swap
|
||||
else:
|
||||
mode_map["I;16"] = _PyAccessI16_L
|
||||
mode_map["I;16L"] = _PyAccessI16_L
|
||||
mode_map["I;16B"] = _PyAccessI16_N
|
||||
|
||||
mode_map["I;32L"] = _PyAccessI32_Swap
|
||||
mode_map["I;32B"] = _PyAccessI32_N
|
||||
|
||||
|
||||
def new(img, readonly=False):
|
||||
access_type = mode_map.get(img.mode, None)
|
||||
if not access_type:
|
||||
logger.debug("PyAccess Not Implemented: %s", img.mode)
|
||||
return None
|
||||
return access_type(img, readonly)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user