diff --git a/packaging/macos/osxdeploy.sh b/packaging/macos/osxdeploy.sh index b181e99230..3973854861 100755 --- a/packaging/macos/osxdeploy.sh +++ b/packaging/macos/osxdeploy.sh @@ -1,713 +1,724 @@ #!/usr/bin/env bash # Krita tool to create dmg from installed source # Copies all files to a folder to be converted into the final dmg # osxdeploy.sh automates the creation of the release DMG. # default background and style are used if none provided # A short explanation of what it does: # - Copies krita.app contents to kritadmg folder # - Copies i/share to Contents/Resources excluding unnecessary files # - Copies translations, qml and quicklook PlugIns # - Copies i/plugins and i/lib/plugins to Contents/PlugIns # - Runs macdeployqt: macdeployqt is not built by default in ext_qt # build by: # cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src # make sub-macdeployqt-all # make sub-macdeployqt-install_subtargets # make install # the script changes dir to installation/bin to run macdeployqt as it can be buggy # if not run from the same folder as the binary is on. # - Fix rpath from krita bin # - Find missing libraries from plugins and copy to Frameworks or plugins. # This uses oTool iterative to find all unique libraries, then it searches each # library fond in folder, and if not found attempts to copy contents # to the appropriate folder, either Frameworks (if frameworks is in namefile, or # library has plugin isnot in path), or plugin if otherwise. # - Builds DMG # Building DMG creates a new dmg with the contents of # mounts the dmg and sets the style for dmg. # unmount # Compress resulting dmg into krita_nightly-.dmg # deletes temporary files. if test -z ${BUILDROOT}; then echo "ERROR: BUILDROOT env not set!" echo "\t Must point to the root of the buildfiles as stated in 3rdparty Readme" echo "exiting..." exit fi # print status messages print_msg() { printf "\e[32m${1}\e[0m\n" "${@:2}" # printf "%s\n" "${1}" >> ${OUPUT_LOG} } # print error print_error() { printf "\e[31m%s %s\e[0m\n" "Error:" "${1}" } get_script_dir() { script_source="${BASH_SOURCE[0]}" # go to target until finding root. while [ -L "${script_source}" ]; do script_target="$(readlink ${script_source})" if [[ "${script_source}" = /* ]]; then script_source="$script_target" else script_dir="$(dirname "${script_source}")" script_source="${script_dir}/${script_target}" fi done echo "$(dirname ${script_source})" } DMG_title="krita" #if changed krita.temp.dmg must be deleted manually SCRIPT_SOURCE_DIR="$(get_script_dir)" # There is some duplication between build and deploy scripts # a config env file could would be a nice idea. KIS_SRC_DIR=${BUILDROOT}/krita KIS_INSTALL_DIR=${BUILDROOT}/i KIS_BUILD_DIR=${BUILDROOT}/kisbuild # only used for getting git sha number KRITA_DMG=${BUILDROOT}/kritadmg KRITA_DMG_TEMPLATE=${BUILDROOT}/kritadmg-template export PATH=${KIS_INSTALL_DIR}/bin:$PATH # flags for OSX environment # We only support from 10.11 up export MACOSX_DEPLOYMENT_TARGET=10.11 export QMAKE_MACOSX_DEPLOYMENT_TARGET=10.11 print_usage () { printf "USAGE: osxdeploy.sh [-s=] [-notarize-ac=] [-style=] [-bg=] -s \t\t\t Code sign identity for codesign -notarize-ac \t Apple account name for notarization purposes \t\t\t script will attempt to get password from keychain, if fails provide one with \t\t\t the -notarize-pass option: To add a password run \t\t\t security add-generic-password -a \"AC_USERNAME\" -w -s \"KRITA_AC_PASS\" -notarize-pass \t If given, the Apple account password. Otherwise an attempt will be macdeployqt_exists \t\t\t to get the password from keychain using the account given in option. + -asc-provider \t some AppleIds might need this option pass the + -style \t\t Style file defined from 'dmgstyle.sh' output -bg \t\t Set a background image for dmg folder. \t\t\t osxdeploy needs an input image to attach to the dmg background \t\t\t image recommended size is at least 950x500 " } # Attempt to detach previous mouted DMG if [[ -d "/Volumes/${DMG_title}" ]]; then echo "WARNING: Another Krita DMG is mounted!" echo "Attempting eject…" hdiutil detach "/Volumes/${DMG_title}" if [ $? -ne 0 ]; then exit fi echo "Success!" fi # -- Parse input args for arg in "${@}"; do if [ "${arg}" = -bg=* -a -f "${arg#*=}" ]; then DMG_validBG=0 bg_filename=${arg#*=} echo "attempting to check background is valid jpg or png..." BG_FORMAT=$(sips --getProperty format ${bg_filename} | awk '{printf $2}') if [[ "png" = ${BG_FORMAT} || "jpeg" = ${BG_FORMAT} ]];then echo "valid image file" DMG_background=$(cd "$(dirname "${bg_filename}")"; pwd -P)/$(basename "${bg_filename}") DMG_validBG=1 # check imageDPI BG_DPI=$(sips --getProperty dpiWidth ${DMG_background} | grep dpi | awk '{print $2}') if [[ $(echo "${BG_DPI} > 150" | bc -l) -eq 1 ]]; then printf "WARNING: image dpi has an effect on apparent size! Check dpi is adequate for screen display if image appears very small Current dpi is: %s\n" ${BG_DPI} fi fi fi # If string starts with -sign if [[ ${arg} = -s=* ]]; then CODE_SIGNATURE="${arg#*=}" fi if [[ ${arg} = -notarize-ac=* ]]; then NOTARIZE_ACC="${arg#*=}" fi if [[ ${arg} = -notarize-pass=* ]]; then NOTARIZE_PASS="${arg#*=}" fi if [[ ${arg} = -style=* ]]; then style_filename="${arg#*=}" if [[ -f "${style_filename}" ]]; then DMG_STYLE="${style_filename}" fi fi + if [[ ${arg} = -asc-provider=* ]]; then + ASC_PROVIDER="${arg#*=}" + fi + if [[ ${arg} = "-h" || ${arg} = "--help" ]]; then print_usage exit fi done # -- Checks and messages ### PYTHONAttempt to find python_version local_PY_MAYOR_VERSION=$(python -c "import sys; print(sys.version_info[0])") local_PY_MINOR_VERSION=$(python -c "import sys; print(sys.version_info[1])") PY_VERSION="${local_PY_MAYOR_VERSION}.${local_PY_MINOR_VERSION}" print_msg "Detected Python %s" "${PY_VERSION}" ### Code Signature & NOTARIZATION NOTARIZE="false" if [[ -z "${CODE_SIGNATURE}" ]]; then echo "WARNING: No code signature provided, Code will not be signed" else print_msg "Code will be signed with %s" "${CODE_SIGNATURE}" ### NOTARIZATION if [[ -n "${NOTARIZE_ACC}" ]]; then security find-generic-password -s "KRITA_AC_PASS" > /dev/null 2>&1 if [[ ${?} -eq 0 || -n "${NOTARIZE_PASS}" ]]; then NOTARIZE="true" else echo "No password given for notarization or KRITA_AC_PASS missig in keychain" fi fi fi if [[ ${NOTARIZE} = "true" ]]; then print_msg "Notarization checks complete, This build will be notarized" else echo "WARNING: Account information missing, Notarization will not be performed" fi ### STYLE for DMG if [[ ! ${DMG_STYLE} ]]; then DMG_STYLE="${SCRIPT_SOURCE_DIR}/default.style" fi print_msg "Using style from: %s" "${DMG_STYLE}" ### Background for DMG if [[ ${DMG_validBG} -eq 0 ]]; then echo "No jpg or png valid file detected!!" echo "Using default style" DMG_background="${SCRIPT_SOURCE_DIR}/krita_dmgBG.jpg" fi # Helper functions countArgs () { echo "${#}" } stringContains () { echo "$(grep "${2}" <<< "${1}")" } waiting_fixed() { local message="${1}" local waitTime=${2} for i in $(seq ${waitTime}); do sleep 1 printf -v dots '%*s' ${i} printf -v spaces '%*s' $((${waitTime} - $i)) printf "\r%s [%s%s]" "${message}" "${dots// /.}" "${spaces}" done printf "\n" } add_lib_to_list() { local llist=${2} if test -z "$(grep ${1##*/} <<< ${llist})" ; then local llist="${llist} ${1##*/} " fi echo "${llist}" } # Find all @rpath and Absolute to buildroot path libs # Add to libs_used # converts absolute buildroot path to @rpath find_needed_libs () { # echo "Analyzing libraries with oTool..." >&2 local libs_used="" # input lib_lists founded for libFile in ${@}; do if test -z "$(file ${libFile} | grep 'Mach-O')" ; then # echo "skipping ${libFile}" >&2 continue fi oToolResult=$(otool -L ${libFile} | awk '{print $1}') resultArray=(${oToolResult}) # convert to array for lib in ${resultArray[@]:1}; do if test "${lib:0:1}" = "@"; then local libs_used=$(add_lib_to_list "${lib}" "${libs_used}") fi if [[ "${lib:0:${#BUILDROOT}}" = "${BUILDROOT}" ]]; then printf "Fixing %s: %s\n" "${libFile#${KRITA_DMG}/}" "${lib##*/}" >&2 if [[ "${lib##*/}" = "${libFile##*/}" ]]; then install_name_tool -id ${lib##*/} "${libFile}" else install_name_tool -change ${lib} "@rpath/${lib##*${BUILDROOT}/i/lib/}" "${libFile}" local libs_used=$(add_lib_to_list "${lib}" "${libs_used}") fi fi done done echo "${libs_used}" # return updated list } find_missing_libs (){ # echo "Searching for missing libs on deployment folders…" >&2 local libs_missing="" for lib in ${@}; do if test -z "$(find ${KRITA_DMG}/krita.app/Contents/ -name ${lib})"; then # echo "Adding ${lib} to missing libraries." >&2 libs_missing="${libs_missing} ${lib}" fi done echo "${libs_missing}" } copy_missing_libs () { for lib in ${@}; do result=$(find -L "${BUILDROOT}/i" -name "${lib}") if test $(countArgs ${result}) -eq 1; then if [ "$(stringContains "${result}" "plugin")" ]; then cp -pv ${result} ${KRITA_DMG}/krita.app/Contents/PlugIns/ krita_findmissinglibs "${KRITA_DMG}/krita.app/Contents/PlugIns/${result##*/}" else cp -pv ${result} ${KRITA_DMG}/krita.app/Contents/Frameworks/ krita_findmissinglibs "${KRITA_DMG}/krita.app/Contents/Frameworks/${result##*/}" fi else echo "${lib} might be a missing framework" if [ "$(stringContains "${result}" "framework")" ]; then echo "copying framework ${BUILDROOT}/i/lib/${lib}.framework to dmg" # rsync only included ${lib} Resources Versions rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/${lib} ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/Resources ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ rsync -priul ${BUILDROOT}/i/lib/${lib}.framework/Versions ${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/ krita_findmissinglibs "$(find "${KRITA_DMG}/krita.app/Contents/Frameworks/${lib}.framework/" -type f -perm 755)" fi fi done } krita_findmissinglibs() { neededLibs=$(find_needed_libs "${@}") missingLibs=$(find_missing_libs ${neededLibs}) if test $(countArgs ${missingLibs}) -gt 0; then printf "Found missing libs: %s\n" "${missingLibs}" copy_missing_libs ${missingLibs} fi } strip_python_dmginstall() { # reduce size of framework python # Removes tests, installers, pyenv, distutils echo "Removing unnecessary files from Python.Framework to be packaged..." PythonFrameworkBase="${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework" cd ${PythonFrameworkBase} find . -name "test*" -type d | xargs rm -rf find "${PythonFrameworkBase}/Versions/${PY_VERSION}/bin" -not -name "python*" \( -type f -or -type l \) | xargs rm -f cd "${PythonFrameworkBase}/Versions/${PY_VERSION}/lib/python${PY_VERSION}" rm -rf distutils tkinter ensurepip venv lib2to3 idlelib } # Some libraries require r_path to be removed # we must not apply delete rpath globally delete_install_rpath() { xargs -P4 -I FILE install_name_tool -delete_rpath "${BUILDROOT}/i/lib" FILE 2> "${BUILDROOT}/deploy_error.log" } fix_python_framework() { # Fix python.framework rpath and slims down installation PythonFrameworkBase="${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework" # Fix main library pythonLib="${PythonFrameworkBase}/Python" install_name_tool -id "${pythonLib##*/}" "${pythonLib}" install_name_tool -add_rpath @loader_path/../../../ "${pythonLib}" 2> /dev/null install_name_tool -change @loader_path/../../../../libintl.9.dylib @loader_path/../../../libintl.9.dylib "${pythonLib}" # Fix all executables install_name_tool -add_rpath @executable_path/../../../../../../../ "${PythonFrameworkBase}/Versions/Current/Resources/Python.app/Contents/MacOS/Python" install_name_tool -change "${KIS_INSTALL_DIR}/lib/Python.framework/Versions/${PY_VERSION}/Python" @executable_path/../../../../../../Python "${PythonFrameworkBase}/Versions/Current/Resources/Python.app/Contents/MacOS/Python" install_name_tool -add_rpath @executable_path/../../../../ "${PythonFrameworkBase}/Versions/Current/bin/python${PY_VERSION}" install_name_tool -add_rpath @executable_path/../../../../ "${PythonFrameworkBase}/Versions/Current/bin/python${PY_VERSION}m" # Fix rpaths from Python.Framework find ${PythonFrameworkBase} -type f -perm 755 | delete_install_rpath find "${PythonFrameworkBase}/Versions/Current/site-packages/PyQt5" -type f -name "*.so" | delete_install_rpath } # Checks for macdeployqt # If not present attempts to install # If it fails shows an informatve message # (For now, macdeployqt is fundamental to deploy) macdeployqt_exists() { printf "Checking for macdeployqt... " if [[ ! -e "${KIS_INSTALL_DIR}/bin/macdeployqt" ]]; then printf "Not Found!\n" printf "Attempting to install macdeployqt\n" cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src make sub-macdeployqt-all make sub-macdeployqt-install_subtargets make install if [[ ! -e "${KIS_INSTALL_DIR}/bin/macdeployqt" ]]; then printf " ERROR: Failed to install macdeployqt! Compile and install from qt source directory Source code to build could be located in qttools/src in qt source dir: ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src From the source dir, build and install: make sub-macdeployqt-all make sub-macdeployqt-install_subtargets make install " printf "\nexiting...\n" exit else echo "Done!" fi else echo "Found!" fi } krita_deploy () { # check for macdeployqt macdeployqt_exists cd ${BUILDROOT} # Update files in krita.app echo "Deleting previous kritadmg run..." rm -rf ./krita.dmg ${KRITA_DMG} # Copy new builtFiles echo "Preparing ${KRITA_DMG} for deployment..." echo "Copying krita.app..." mkdir "${KRITA_DMG}" rsync -prul ${KIS_INSTALL_DIR}/bin/krita.app ${KRITA_DMG} mkdir -p ${KRITA_DMG}/krita.app/Contents/PlugIns mkdir -p ${KRITA_DMG}/krita.app/Contents/Frameworks echo "Copying share..." # Deletes old copies of translation and qml to be recreated cd ${KIS_INSTALL_DIR}/share/ rsync -prul --delete ./ \ --exclude krita_SRCS.icns \ --exclude aclocal \ --exclude doc \ --exclude ECM \ --exclude eigen3 \ --exclude emacs \ --exclude gettext \ --exclude gettext-0.19.8 \ --exclude info \ --exclude kf5 \ --exclude kservices5 \ --exclude man \ --exclude ocio \ --exclude pkgconfig \ --exclude mime \ --exclude translations \ --exclude qml \ ${KRITA_DMG}/krita.app/Contents/Resources cd ${BUILDROOT} echo "Copying translations..." rsync -prul ${KIS_INSTALL_DIR}/translations/ \ ${KRITA_DMG}/krita.app/Contents/Resources/translations echo "Copying kritaquicklook..." mkdir -p ${KRITA_DMG}/krita.app/Contents/Library/QuickLook rsync -prul ${KIS_INSTALL_DIR}/plugins/kritaquicklook.qlgenerator ${KRITA_DMG}/krita.app/Contents/Library/QuickLook cd ${KRITA_DMG}/krita.app/Contents ln -shF Resources share echo "Copying qml..." rsync -prul ${KIS_INSTALL_DIR}/qml Resources/qml echo "Copying plugins..." # exclude kritaquicklook.qlgenerator/ cd ${KIS_INSTALL_DIR}/plugins/ rsync -prul --delete --delete-excluded ./ \ --exclude kritaquicklook.qlgenerator \ ${KRITA_DMG}/krita.app/Contents/PlugIns cd ${BUILDROOT} rsync -prul ${KIS_INSTALL_DIR}/lib/kritaplugins/ ${KRITA_DMG}/krita.app/Contents/PlugIns # rsync -prul {KIS_INSTALL_DIR}/lib/libkrita* Frameworks/ # To avoid errors macdeployqt must be run from bin location # ext_qt will not build macdeployqt by default so it must be build manually # cd ${BUILDROOT}/depbuild/ext_qt/ext_qt-prefix/src/ext_qt/qttools/src # make sub-macdeployqt-all # make sub-macdeployqt-install_subtargets # make install echo "Running macdeployqt..." cd ${KIS_INSTALL_DIR}/bin ./macdeployqt ${KRITA_DMG}/krita.app \ -verbose=0 \ -executable=${KRITA_DMG}/krita.app/Contents/MacOS/krita \ -libpath=${KIS_INSTALL_DIR}/lib \ -qmldir=${KIS_INSTALL_DIR}/qml \ # -extra-plugins=${KIS_INSTALL_DIR}/lib/kritaplugins \ # -extra-plugins=${KIS_INSTALL_DIR}/lib/plugins \ # -extra-plugins=${KIS_INSTALL_DIR}/plugins cd ${BUILDROOT} echo "macdeployqt done!" echo "Copying python..." # Copy this framework last! # It is best that macdeployqt does not modify Python.framework # folders with period in name are treated as Frameworks for codesign rsync -prul ${KIS_INSTALL_DIR}/lib/Python.framework ${KRITA_DMG}/krita.app/Contents/Frameworks/ rsync -prul ${KIS_INSTALL_DIR}/lib/krita-python-libs ${KRITA_DMG}/krita.app/Contents/Frameworks/ # change perms on Python to allow header change chmod +w ${KRITA_DMG}/krita.app/Contents/Frameworks/Python.framework/Python fix_python_framework strip_python_dmginstall # fix python pyc # precompile all pyc so the dont alter signature echo "Precompiling all python files..." cd ${KRITA_DMG}/krita.app ${KIS_INSTALL_DIR}/bin/python -m compileall . &> /dev/null install_name_tool -delete_rpath @loader_path/../../../../lib ${KRITA_DMG}/krita.app/Contents/MacOS/krita rm -rf ${KRITA_DMG}/krita.app/Contents/PlugIns/kf5/org.kde.kwindowsystem.platforms # repair krita for plugins printf "Searching for missing libraries\n" krita_findmissinglibs $(find ${KRITA_DMG}/krita.app/Contents -type f -perm 755 -or -name "*.dylib" -or -name "*.so") printf "removing absolute or broken linksys, if any\n" find "${KRITA_DMG}/krita.app/Contents" -type l \( -lname "/*" -or -not -exec test -e {} \; \) -print | xargs rm echo "Done!" } # helper to define function only once batch_codesign() { xargs -P4 -I FILE codesign --options runtime --timestamp -f -s "${CODE_SIGNATURE}" FILE } # Code sign must be done as recommended by apple "sign code inside out in individual stages" signBundle() { cd ${KRITA_DMG} # sign Frameworks and libs cd ${KRITA_DMG}/krita.app/Contents/Frameworks # remove debug version as both versions can't be signed. rm ${KRITA_DMG}/krita.app/Contents/Frameworks/QtScript.framework/Versions/Current/QtScript_debug find . -type f -perm 755 -or -name "*.dylib" -or -name "*.so" | batch_codesign find . -type d -name "*.framework" | xargs printf "%s/Versions/Current\n" | batch_codesign # Sign all other files in Framework (needed) # there are many files in python do we need to sign them all? find krita-python-libs -type f | batch_codesign # find python -type f | batch_codesign # Sing only libraries and plugins cd ${KRITA_DMG}/krita.app/Contents/PlugIns find . -type f | batch_codesign cd ${KRITA_DMG}/krita.app/Contents/Library/QuickLook printf "kritaquicklook.qlgenerator" | batch_codesign # It is recommended to sign every Resource file cd ${KRITA_DMG}/krita.app/Contents/Resources find . -type f | batch_codesign #Finally sign krita and krita.app printf "${KRITA_DMG}/krita.app/Contents/MacOS/krita" | batch_codesign printf "${KRITA_DMG}/krita.app" | batch_codesign } # Notarize build on macOS servers # based on https://github.com/Beep6581/RawTherapee/blob/6fa533c40b34dec527f1176d47cc6c683422a73f/tools/osx/macosx_bundle.sh#L225-L250 notarize_build() { local NOT_SRC_DIR=${1} local NOT_SRC_FILE=${2} if [[ ${NOTARIZE} = "true" ]]; then printf "performing notarization of %s\n" "${2}" cd "${NOT_SRC_DIR}" if [[ -z "${NOTARIZE_PASS}" ]]; then NOTARIZE_PASS="@keychain:KRITA_AC_PASS" fi + ASC_PROVIDER_OP="" + if [[ -n "${ASC_PROVIDER}" ]]; then + ASC_PROVIDER_OP="--asc-provider ${ASC_PROVIDER}" + fi + ditto -c -k --sequesterRsrc --keepParent "${NOT_SRC_FILE}" "${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip" # echo "xcrun altool --notarize-app --primary-bundle-id \"org.krita\" --username \"${NOTARIZE_ACC}\" --password \"${NOTARIZE_PASS}\" --file \"${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip\"" - local altoolResponse="$(xcrun altool --notarize-app --primary-bundle-id "org.krita" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" --file "${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip" 2>&1)" + local altoolResponse="$(xcrun altool --notarize-app --primary-bundle-id "org.krita" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" ${ASC_PROVIDER_OP} --file "${BUILDROOT}/tmp_notarize/${NOT_SRC_FILE}.zip" 2>&1)" if [[ -n "$(grep 'Error' <<< ${altoolResponse})" ]]; then printf "ERROR: xcrun altool exited with the following error! \n\n%s\n\n" "${altoolResponse}" printf "This could mean there is an error in AppleID authentication!\n" printf "aborting notarization\n" NOTARIZE="false" return else printf "Response:\n\n%s\n\n" "${altoolResponse}" fi - local uuid="$(grep 'RequestUUID' <<< ${altoolResponse} | awk '{ print $3 }')" + local uuid="$(grep 'RequestUUID' <<< ${altoolResponse} | awk '{ print $NF }')" echo "RequestUUID = ${uuid}" # Display identifier string - waiting_fixed "Waiting to retrieve notarize status" 15 + waiting_fixed "Waiting to retrieve notarize status" 30 while true ; do - fullstatus=$(xcrun altool --notarization-info "${uuid}" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" 2>&1) # get the status + fullstatus=$(xcrun altool --notarization-info "${uuid}" --username "${NOTARIZE_ACC}" --password "${NOTARIZE_PASS}" ${ASC_PROVIDER_OP} 2>&1) # get the status notarize_status=`echo "${fullstatus}" | grep 'Status\:' | awk '{ print $2 }'` echo "${fullstatus}" if [[ "${notarize_status}" = "success" ]]; then xcrun stapler staple "${NOT_SRC_FILE}" #staple the ticket xcrun stapler validate -v "${NOT_SRC_FILE}" print_msg "Notarization success!" break elif [[ "${notarize_status}" = "in" ]]; then - waiting_fixed "Notarization still in progress, sleeping for 15 seconds and trying again" 15 + waiting_fixed "Notarization still in progress, sleeping for 20 seconds and trying again" 20 else echo "Notarization failed! full status below" echo "${fullstatus}" exit 1 fi done fi } createDMG () { printf "Creating of dmg with contents of %s...\n" "${KRITA_DMG}" cd ${BUILDROOT} DMG_size=700 ## Build dmg from folder # create dmg on local system # usage of -fsargs minimze gaps at front of filesystem (reduce size) hdiutil create -srcfolder "${KRITA_DMG}" -volname "${DMG_title}" -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${DMG_size}m krita.temp.dmg # Next line is only useful if we have a dmg as a template! # previous hdiutil must be uncommented # cp krita-template.dmg krita.dmg device=$(hdiutil attach -readwrite -noverify -noautoopen "krita.temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') # rsync -priul --delete ${KRITA_DMG}/krita.app "/Volumes/${DMG_title}" # Set style for dmg if [[ ! -d "/Volumes/${DMG_title}/.background" ]]; then mkdir "/Volumes/${DMG_title}/.background" fi cp -v ${DMG_background} "/Volumes/${DMG_title}/.background/" mkdir "/Volumes/${DMG_title}/Terms of Use" cp -v "${KIS_SRC_DIR}/packaging/macos/Terms_of_use.rtf" "/Volumes/${DMG_title}/Terms of Use/" ln -s "/Applications" "/Volumes/${DMG_title}/Applications" ## Apple script to set style style="$(<"${DMG_STYLE}")" printf "${style}" "${DMG_title}" "${DMG_background##*/}" | osascript #Set Icon for DMG cp -v "${SCRIPT_SOURCE_DIR}/KritaIcon.icns" "/Volumes/${DMG_title}/.VolumeIcon.icns" SetFile -a C "/Volumes/${DMG_title}" chmod -Rf go-w "/Volumes/${DMG_title}" # ensure all writing operations to dmg are over sync hdiutil detach $device hdiutil convert "krita.temp.dmg" -format UDZO -imagekey -zlib-level=9 -o krita-out.dmg # Add git version number GIT_SHA=$(grep "#define KRITA_GIT_SHA1_STRING" ${KIS_BUILD_DIR}/libs/version/kritagitversion.h | awk '{gsub(/"/, "", $3); printf $3}') mv krita-out.dmg krita-nightly_${GIT_SHA}.dmg echo "moved krita-out.dmg to krita-nightly_${GIT_SHA}.dmg" rm krita.temp.dmg if [[ -n "${CODE_SIGNATURE}" ]]; then printf "krita-nightly_${GIT_SHA}.dmg" | batch_codesign fi notarize_build ${BUILDROOT} "krita-nightly_${GIT_SHA}.dmg" echo "dmg done!" } ####################### # Program starts!! ######################## # Run deploy command, installation is assumed to exist in BUILDROOT/i krita_deploy # Code sign krita.app if signature given if [[ -n "${CODE_SIGNATURE}" ]]; then signBundle fi notarize_build ${KRITA_DMG} krita.app # Create DMG from files inside ${KRITA_DMG} folder createDMG if [[ "${NOTARIZE}" = "false" ]]; then macosVersion="$(sw_vers | grep ProductVersion | awk ' BEGIN { FS = "[ .\t]" } { print $3} ')" if (( ${macosVersion} == 15 )); then print_error "Build not notarized! Needed for macOS versions above 10.14" fi fi diff --git a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp index c6791f7397..9fd5578c6d 100644 --- a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp @@ -1,723 +1,731 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_free_transform_strategy.h" #include #include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" #include "kis_algebra_2d.h" enum StrokeFunction { ROTATE = 0, MOVE, RIGHTSCALE, TOPRIGHTSCALE, TOPSCALE, TOPLEFTSCALE, LEFTSCALE, BOTTOMLEFTSCALE, BOTTOMSCALE, BOTTOMRIGHTSCALE, BOTTOMSHEAR, RIGHTSHEAR, TOPSHEAR, LEFTSHEAR, MOVECENTER, PERSPECTIVE }; struct KisFreeTransformStrategy::Private { Private(KisFreeTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), - imageTooBig(false) + imageTooBig(false), + isTransforming(false) { scaleCursors[0] = KisCursor::sizeHorCursor(); scaleCursors[1] = KisCursor::sizeFDiagCursor(); scaleCursors[2] = KisCursor::sizeVerCursor(); scaleCursors[3] = KisCursor::sizeBDiagCursor(); scaleCursors[4] = KisCursor::sizeHorCursor(); scaleCursors[5] = KisCursor::sizeFDiagCursor(); scaleCursors[6] = KisCursor::sizeVerCursor(); scaleCursors[7] = KisCursor::sizeBDiagCursor(); shearCursorPixmap.load(":/shear_cursor.png"); } KisFreeTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { QPointF topLeft; QPointF topMiddle; QPointF topRight; QPointF middleLeft; QPointF rotationCenter; QPointF middleRight; QPointF bottomLeft; QPointF bottomMiddle; QPointF bottomRight; }; HandlePoints transformedHandles; QTransform transform; QCursor scaleCursors[8]; // cursors for the 8 directions QPixmap shearCursorPixmap; bool imageTooBig; ToolTransformArgs clickArgs; QPointF clickPos; + bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); }; KisFreeTransformStrategy::KisFreeTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisFreeTransformStrategy::~KisFreeTransformStrategy() { } void KisFreeTransformStrategy::Private::recalculateTransformedHandles() { transformedHandles.topLeft = transform.map(transaction.originalTopLeft()); transformedHandles.topMiddle = transform.map(transaction.originalMiddleTop()); transformedHandles.topRight = transform.map(transaction.originalTopRight()); transformedHandles.middleLeft = transform.map(transaction.originalMiddleLeft()); transformedHandles.rotationCenter = transform.map(currentArgs.originalCenter() + currentArgs.rotationCenterOffset()); transformedHandles.middleRight = transform.map(transaction.originalMiddleRight()); transformedHandles.bottomLeft = transform.map(transaction.originalBottomLeft()); transformedHandles.bottomMiddle = transform.map(transaction.originalMiddleBottom()); transformedHandles.bottomRight = transform.map(transaction.originalBottomRight()); } void KisFreeTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { if (perspectiveModifierActive && !m_d->transaction.shouldAvoidPerspectiveTransform()) { m_d->function = PERSPECTIVE; return; } QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); qreal rotationHandleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : ROTATE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); handleChooser.addFunction(m_d->transformedHandles.topMiddle, handleRadius, TOPSCALE); handleChooser.addFunction(m_d->transformedHandles.topRight, handleRadius, TOPRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleRight, handleRadius, RIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomRight, handleRadius, BOTTOMRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomMiddle, handleRadius, BOTTOMSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomLeft, handleRadius, BOTTOMLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleLeft, handleRadius, LEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.topLeft, handleRadius, TOPLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.rotationCenter, rotationHandleRadius, MOVECENTER); m_d->function = handleChooser.function(); if (m_d->function == ROTATE || m_d->function == MOVE) { QRectF originalRect = m_d->transaction.originalRect(); QPointF t = m_d->transform.inverted().map(mousePos); if (t.x() >= originalRect.left() && t.x() <= originalRect.right()) { if (fabs(t.y() - originalRect.top()) <= handleRadius) m_d->function = TOPSHEAR; if (fabs(t.y() - originalRect.bottom()) <= handleRadius) m_d->function = BOTTOMSHEAR; } if (t.y() >= originalRect.top() && t.y() <= originalRect.bottom()) { if (fabs(t.x() - originalRect.left()) <= handleRadius) m_d->function = LEFTSHEAR; if (fabs(t.x() - originalRect.right()) <= handleRadius) m_d->function = RIGHTSHEAR; } } } QCursor KisFreeTransformStrategy::Private::getScaleCursor(const QPointF &handlePt) { QPointF handlePtInWidget = converter->imageToWidget(handlePt); QPointF centerPtInWidget = converter->imageToWidget(currentArgs.transformedCenter()); QPointF direction = handlePtInWidget - centerPtInWidget; qreal angle = atan2(direction.y(), direction.x()); angle = normalizeAngle(angle); int octant = qRound(angle * 4. / M_PI) % 8; return scaleCursors[octant]; } QCursor KisFreeTransformStrategy::Private::getShearCursor(const QPointF &start, const QPointF &end) { QPointF startPtInWidget = converter->imageToWidget(start); QPointF endPtInWidget = converter->imageToWidget(end); QPointF direction = endPtInWidget - startPtInWidget; qreal angle = atan2(-direction.y(), direction.x()); return QCursor(shearCursorPixmap.transformed(QTransform().rotateRadians(-angle))); } QCursor KisFreeTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case MOVE: cursor = KisCursor::moveCursor(); break; case ROTATE: cursor = KisCursor::rotateCursor(); break; case PERSPECTIVE: //TODO: find another cursor for perspective cursor = KisCursor::rotateCursor(); break; case RIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleRight); break; case TOPSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topMiddle); break; case LEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleLeft); break; case BOTTOMSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomMiddle); break; case TOPRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topRight); break; case BOTTOMLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomLeft); break; case TOPLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topLeft); break; case BOTTOMRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomRight); break; case MOVECENTER: cursor = KisCursor::handCursor(); break; case BOTTOMSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomLeft, m_d->transformedHandles.bottomRight); break; case RIGHTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomRight, m_d->transformedHandles.topRight); break; case TOPSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topRight, m_d->transformedHandles.topLeft); break; case LEFTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topLeft, m_d->transformedHandles.bottomLeft); break; } return cursor; } void KisFreeTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), 0, 0); qreal rX = 1; qreal rY = 1; QRectF rotationCenterRect = KisTransformUtils::handleRect(KisTransformUtils::rotationHandleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), &rX, &rY); QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); handles.addRect(handleRect.translated(m_d->transaction.originalTopLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalTopRight())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleTop())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleBottom())); QPointF rotationCenter = m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset(); QPointF dx(rX + 3, 0); QPointF dy(0, rY + 3); handles.addEllipse(rotationCenterRect.translated(rotationCenter)); handles.moveTo(rotationCenter - dx); handles.lineTo(rotationCenter + dx); handles.moveTo(rotationCenter - dy); handles.lineTo(rotationCenter + dy); gc.save(); + if (m_d->isTransforming) { + gc.setOpacity(0.1); + } + //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); } void KisFreeTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::beginPrimaryAction(const QPointF &pt) { m_d->clickArgs = m_d->currentArgs; m_d->clickPos = pt; return true; } void KisFreeTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActive, bool altModifierActive) { // Note: "shiftModifierActive" just tells us if the shift key is being pressed // Note: "altModifierActive" just tells us if the alt key is being pressed + m_d->isTransforming = true; const QPointF anchorPoint = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); switch (m_d->function) { case MOVE: { QPointF diff = mousePos - m_d->clickPos; if (shiftModifierActive) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.S * m.projectedP; QPointF originalDiff = t.inverted().map(diff); if (qAbs(originalDiff.x()) >= qAbs(originalDiff.y())) { originalDiff.setY(0); } else { originalDiff.setX(0); } diff = t.map(originalDiff); } m_d->currentArgs.setTransformedCenter(m_d->clickArgs.transformedCenter() + diff); break; } case ROTATE: { const KisTransformUtils::MatricesPack clickM(m_d->clickArgs); const QTransform clickT = clickM.finalTransform(); const QPointF rotationCenter = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); const QPointF clickMouseImagePos = clickT.inverted().map(m_d->clickPos) - rotationCenter; const QPointF mouseImagePos = clickT.inverted().map(mousePos) - rotationCenter; const qreal a1 = atan2(clickMouseImagePos.y(), clickMouseImagePos.x()); const qreal a2 = atan2(mouseImagePos.y(), mouseImagePos.x()); const qreal theta = KisAlgebra2D::signZZ(clickT.determinant()) * (a2 - a1); // Snap with shift key if (shiftModifierActive) { const qreal snapAngle = M_PI_4 / 6.0; // fifteen degrees qint32 thetaIndex = static_cast((theta / snapAngle) + 0.5); m_d->currentArgs.setAZ(normalizeAngle(thetaIndex * snapAngle)); } else { m_d->currentArgs.setAZ(normalizeAngle(m_d->clickArgs.aZ() + theta)); } KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case PERSPECTIVE: { QPointF diff = mousePos - m_d->clickPos; double thetaX = - diff.y() * M_PI / m_d->transaction.originalHalfHeight() / 2 / fabs(m_d->currentArgs.scaleY()); m_d->currentArgs.setAX(normalizeAngle(m_d->clickArgs.aX() + thetaX)); qreal sign = qAbs(m_d->currentArgs.aX() - M_PI) < M_PI / 2 ? -1.0 : 1.0; double thetaY = sign * diff.x() * M_PI / m_d->transaction.originalHalfWidth() / 2 / fabs(m_d->currentArgs.scaleX()); m_d->currentArgs.setAY(normalizeAngle(m_d->clickArgs.aY() + thetaY)); KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); KisTransformUtils::MatricesPack clickM(m_d->clickArgs); QTransform clickT = clickM.finalTransform(); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case TOPSCALE: case BOTTOMSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == TOPSCALE) { staticPoint = m_d->transaction.originalMiddleBottom(); movingPoint = m_d->transaction.originalMiddleTop(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleTop(); movingPoint = m_d->transaction.originalMiddleBottom(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.y(), movingPoint.y())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.y() <= staticPoint.y() ? -extraSign : extraSign; m_d->currentArgs.setScaleY(sign * m_d->currentArgs.scaleY()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleY(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleX() / m_d->clickArgs.scaleY(); m_d->currentArgs.setScaleX(aspectRatio * result.scale); } m_d->currentArgs.setScaleY(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case LEFTSCALE: case RIGHTSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == LEFTSCALE) { staticPoint = m_d->transaction.originalMiddleRight(); movingPoint = m_d->transaction.originalMiddleLeft(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleLeft(); movingPoint = m_d->transaction.originalMiddleRight(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.x(), movingPoint.x())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.x() <= staticPoint.x() ? -extraSign : extraSign; m_d->currentArgs.setScaleX(sign * m_d->currentArgs.scaleX()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleX(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleY() / m_d->clickArgs.scaleX(); m_d->currentArgs.setScaleY(aspectRatio * result.scale); } m_d->currentArgs.setScaleX(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case TOPRIGHTSCALE: case BOTTOMRIGHTSCALE: case TOPLEFTSCALE: case BOTTOMLEFTSCALE: { QPointF staticPoint; QPointF movingPoint; if (m_d->function == TOPRIGHTSCALE) { staticPoint = m_d->transaction.originalBottomLeft(); movingPoint = m_d->transaction.originalTopRight(); } else if (m_d->function == BOTTOMRIGHTSCALE) { staticPoint = m_d->transaction.originalTopLeft(); movingPoint = m_d->transaction.originalBottomRight(); } else if (m_d->function == TOPLEFTSCALE) { staticPoint = m_d->transaction.originalBottomRight(); movingPoint = m_d->transaction.originalTopLeft(); } else { staticPoint = m_d->transaction.originalTopRight(); movingPoint = m_d->transaction.originalBottomLeft(); } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !(qFuzzyCompare(anchorPoint.x(), movingPoint.x()) || qFuzzyCompare(anchorPoint.y(), movingPoint.y()))) { staticPoint = anchorPoint; } QPointF staticPointInView = m_d->transform.map(staticPoint); QPointF movingPointInView = mousePos; if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.finalTransform(); QPointF refDiff = t.map(movingPoint) - staticPointInView; QPointF realDiff = mousePos - staticPointInView; realDiff = kisProjectOnVector(refDiff, realDiff); movingPointInView = staticPointInView + realDiff; } GSL::ScaleResult2D result = GSL::calculateScale2D(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, movingPointInView); m_d->currentArgs.setScaleX(result.scaleX); m_d->currentArgs.setScaleY(result.scaleY); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case MOVECENTER: { QPointF pt = m_d->transform.inverted().map(mousePos); pt = KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect()); QPointF newRotationCenterOffset = pt - m_d->currentArgs.originalCenter(); if (shiftModifierActive) { if (qAbs(newRotationCenterOffset.x()) > qAbs(newRotationCenterOffset.y())) { newRotationCenterOffset.ry() = 0; } else { newRotationCenterOffset.rx() = 0; } } m_d->currentArgs.setRotationCenterOffset(newRotationCenterOffset); emit requestResetRotationCenterButtons(); } break; case TOPSHEAR: case BOTTOMSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == BOTTOMSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dx = sign * m_d->clickArgs.shearX() * m_d->clickArgs.scaleY() * m_d->transaction.originalHalfHeight(); // get the dx pixels corresponding to the current shearX factor dx += diff.x(); // calculate the new shearX factor m_d->currentArgs.setShearX(sign * dx / m_d->currentArgs.scaleY() / m_d->transaction.originalHalfHeight()); // calculate the new shearX factor break; } case LEFTSHEAR: case RIGHTSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == RIGHTSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dy = sign * m_d->clickArgs.shearY() * m_d->clickArgs.scaleX() * m_d->transaction.originalHalfWidth(); dy += diff.y(); // calculate the new shearY factor m_d->currentArgs.setShearY(sign * dy / m_d->clickArgs.scaleX() / m_d->transaction.originalHalfWidth()); break; } } m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; + m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisFreeTransformStrategy::Private::recalculateTransformations() { KisTransformUtils::MatricesPack m(currentArgs); QTransform sanityCheckMatrix = m.TS * m.SC * m.S * m.projectedP; /** * The center of the original image should still * stay the origin of CS */ KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(currentArgs.originalCenter()).manhattanLength() < 1e-4); transform = m.finalTransform(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not imageTooBig = KisTransformUtils::checkImageTooBig(transaction.originalRect(), m); // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); } diff --git a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp index 9c23c4d1e3..0cff5ed2a6 100644 --- a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp @@ -1,683 +1,692 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_perspective_transform_strategy.h" #include #include #include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" enum StrokeFunction { DRAG_HANDLE = 0, DRAG_X_VANISHING_POINT, DRAG_Y_VANISHING_POINT, MOVE, NONE }; struct KisPerspectiveTransformStrategy::Private { Private(KisPerspectiveTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), - imageTooBig(false) + imageTooBig(false), + isTransforming(false) { } KisPerspectiveTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { bool xVanishingExists; bool yVanishingExists; QPointF xVanishing; QPointF yVanishing; }; HandlePoints transformedHandles; QTransform transform; QVector srcCornerPoints; QVector dstCornerPoints; int currentDraggingCornerPoint; bool imageTooBig; QPointF clickPos; ToolTransformArgs clickArgs; + bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); void transformIntoArgs(const Eigen::Matrix3f &t); QTransform transformFromArgs(); }; KisPerspectiveTransformStrategy::KisPerspectiveTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisPerspectiveTransformStrategy::~KisPerspectiveTransformStrategy() { } void KisPerspectiveTransformStrategy::Private::recalculateTransformedHandles() { srcCornerPoints.clear(); srcCornerPoints << transaction.originalTopLeft(); srcCornerPoints << transaction.originalTopRight(); srcCornerPoints << transaction.originalBottomLeft(); srcCornerPoints << transaction.originalBottomRight(); dstCornerPoints.clear(); Q_FOREACH (const QPointF &pt, srcCornerPoints) { dstCornerPoints << transform.map(pt); } QMatrix4x4 realMatrix(transform); QVector4D v; v = QVector4D(1, 0, 0, 0); v = realMatrix * v; transformedHandles.xVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.xVanishing = v.toVector2DAffine().toPointF(); v = QVector4D(0, 1, 0, 0); v = realMatrix * v; transformedHandles.yVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.yVanishing = v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { Q_UNUSED(perspectiveModifierActive); QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : NONE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); if (!m_d->transformedHandles.xVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.xVanishing, handleRadius, DRAG_X_VANISHING_POINT); } if (!m_d->transformedHandles.yVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.yVanishing, handleRadius, DRAG_Y_VANISHING_POINT); } m_d->currentDraggingCornerPoint = -1; for (int i = 0; i < m_d->dstCornerPoints.size(); i++) { if (handleChooser.addFunction(m_d->dstCornerPoints[i], handleRadius, DRAG_HANDLE)) { m_d->currentDraggingCornerPoint = i; } } m_d->function = handleChooser.function(); } QCursor KisPerspectiveTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case NONE: cursor = KisCursor::arrowCursor(); break; case MOVE: cursor = KisCursor::moveCursor(); break; case DRAG_HANDLE: case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: cursor = KisCursor::pointingHandCursor(); break; } return cursor; } void KisPerspectiveTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); auto addHandleRectFunc = [&](const QPointF &pt) { handles.addRect( KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), pt) .translated(pt)); }; addHandleRectFunc(m_d->transaction.originalTopLeft()); addHandleRectFunc(m_d->transaction.originalTopRight()); addHandleRectFunc(m_d->transaction.originalBottomLeft()); addHandleRectFunc(m_d->transaction.originalBottomRight()); gc.save(); + if (m_d->isTransforming) { + gc.setOpacity(0.1); + } + /** * WARNING: we cannot install a transform to paint the handles here! * * There is a bug in Qt that prevents painting of cosmetic-pen * brushes in openGL mode when a TxProject matrix is active on * a QPainter. So just convert it manually. * * https://bugreports.qt-project.org/browse/QTBUG-42658 */ //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); { // painting perspective handles QPainterPath perspectiveHandles; QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, QTransform(), m_d->transaction.originalRect(), 0, 0); if (m_d->transformedHandles.xVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.xVanishing); perspectiveHandles.addEllipse(rc); } if (m_d->transformedHandles.yVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.yVanishing); perspectiveHandles.addEllipse(rc); } if (!perspectiveHandles.isEmpty()) { gc.save(); gc.setTransform(m_d->converter->imageToWidgetTransform()); gc.setBrush(Qt::red); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(perspectiveHandles); } gc.restore(); } } } void KisPerspectiveTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::beginPrimaryAction(const QPointF &pt) { Q_UNUSED(pt); if (m_d->function == NONE) return false; m_d->clickPos = pt; m_d->clickArgs = m_d->currentArgs; return true; } Eigen::Matrix3f getTransitionMatrix(const QVector &sp) { Eigen::Matrix3f A; Eigen::Vector3f v3; A << sp[0].x() , sp[1].x() , sp[2].x() ,sp[0].y() , sp[1].y() , sp[2].y() , 1 , 1 , 1; v3 << sp[3].x() , sp[3].y() , 1; Eigen::Vector3f coeffs = A.colPivHouseholderQr().solve(v3); A.col(0) *= coeffs(0); A.col(1) *= coeffs(1); A.col(2) *= coeffs(2); return A; } QTransform toQTransform(const Eigen::Matrix3f &m) { return QTransform(m(0,0), m(1,0), m(2,0), m(0,1), m(1,1), m(2,1), m(0,2), m(1,2), m(2,2)); } Eigen::Matrix3f fromQTransform(const QTransform &t) { Eigen::Matrix3f m; m << t.m11() , t.m21() , t.m31() ,t.m12() , t.m22() , t.m32() ,t.m13() , t.m23() , t.m33(); return m; } Eigen::Matrix3f fromTranslate(const QPointF &pt) { Eigen::Matrix3f m; m << 1 , 0 , pt.x() ,0 , 1 , pt.y() ,0 , 0 , 1; return m; } Eigen::Matrix3f fromScale(qreal sx, qreal sy) { Eigen::Matrix3f m; m << sx , 0 , 0 ,0 , sy , 0 ,0 , 0 , 1; return m; } Eigen::Matrix3f fromShear(qreal sx, qreal sy) { Eigen::Matrix3f m; m << 1 , sx , 0 ,sy , sx*sy + 1, 0 ,0 , 0 , 1; return m; } void KisPerspectiveTransformStrategy::Private::transformIntoArgs(const Eigen::Matrix3f &t) { Eigen::Matrix3f TS = fromTranslate(-currentArgs.originalCenter()); Eigen::Matrix3f m = t * TS.inverse(); qreal tX = m(0,2) / m(2,2); qreal tY = m(1,2) / m(2,2); Eigen::Matrix3f T = fromTranslate(QPointF(tX, tY)); m = T.inverse() * m; // TODO: implement matrix decomposition as described here // https://www.w3.org/TR/css-transforms-1/#decomposing-a-3d-matrix // For now use an extremely hackish approximation if (m(0,1) != 0.0 && m(0,0) != 0.0 && m(2,2) != 0.0) { const qreal factor = (m(1,1) / m(0,1) - m(1,0) / m(0,0)); qreal scaleX = m(0,0) / m(2,2); qreal scaleY = m(0,1) / m(2,2) * factor; Eigen::Matrix3f SC = fromScale(scaleX, scaleY); qreal shearX = 1.0 / factor; qreal shearY = m(1,0) / m(0,0); Eigen::Matrix3f S = fromShear(shearX, shearY); currentArgs.setScaleX(scaleX); currentArgs.setScaleY(scaleY); currentArgs.setShearX(shearX); currentArgs.setShearY(shearY); m = m * SC.inverse(); m = m * S.inverse(); m /= m(2,2); } else { currentArgs.setScaleX(1.0); currentArgs.setScaleY(1.0); currentArgs.setShearX(0.0); currentArgs.setShearY(0.0); } currentArgs.setTransformedCenter(QPointF(tX, tY)); currentArgs.setFlattenedPerspectiveTransform(toQTransform(m)); } QTransform KisPerspectiveTransformStrategy::Private::transformFromArgs() { KisTransformUtils::MatricesPack m(currentArgs); return m.finalTransform(); } QVector4D fromQPointF(const QPointF &pt) { return QVector4D(pt.x(), pt.y(), 0, 1.0); } QPointF toQPointF(const QVector4D &v) { return v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActve, bool altModifierActive) { Q_UNUSED(shiftModifierActve); Q_UNUSED(altModifierActive); + m_d->isTransforming = true; + switch (m_d->function) { case NONE: break; case MOVE: { QPointF diff = mousePos - m_d->clickPos; m_d->currentArgs.setTransformedCenter( m_d->clickArgs.transformedCenter() + diff); break; } case DRAG_HANDLE: { KIS_ASSERT_RECOVER_RETURN(m_d->currentDraggingCornerPoint >=0); m_d->dstCornerPoints[m_d->currentDraggingCornerPoint] = mousePos; Eigen::Matrix3f A = getTransitionMatrix(m_d->srcCornerPoints); Eigen::Matrix3f B = getTransitionMatrix(m_d->dstCornerPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: { QMatrix4x4 m(m_d->transform); QPointF tl = m_d->transaction.originalTopLeft(); QPointF tr = m_d->transaction.originalTopRight(); QPointF bl = m_d->transaction.originalBottomLeft(); QPointF br = m_d->transaction.originalBottomRight(); QVector4D v(1,0,0,0); QVector4D otherV(0,1,0,0); if (m_d->function == DRAG_X_VANISHING_POINT) { v = QVector4D(1,0,0,0); otherV = QVector4D(0,1,0,0); } else { v = QVector4D(0,1,0,0); otherV = QVector4D(1,0,0,0); } QPointF tl_dst = toQPointF(m * fromQPointF(tl)); QPointF tr_dst = toQPointF(m * fromQPointF(tr)); QPointF bl_dst = toQPointF(m * fromQPointF(bl)); QPointF br_dst = toQPointF(m * fromQPointF(br)); QPointF v_dst = toQPointF(m * v); QPointF otherV_dst = toQPointF(m * otherV); QVector srcPoints; QVector dstPoints; QPointF far1_src; QPointF far2_src; QPointF near1_src; QPointF near2_src; QPointF far1_dst; QPointF far2_dst; QPointF near1_dst; QPointF near2_dst; if (m_d->function == DRAG_X_VANISHING_POINT) { // topLeft (far) --- topRight (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, tr_dst)) { far1_src = tl; far2_src = bl; near1_src = tr; near2_src = br; far1_dst = tl_dst; far2_dst = bl_dst; near1_dst = tr_dst; near2_dst = br_dst; // topRight (far) --- topLeft (near) --- vanishing } else { far1_src = tr; far2_src = br; near1_src = tl; near2_src = bl; far1_dst = tr_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = bl_dst; } } else /* if (m_d->function == DRAG_Y_VANISHING_POINT) */{ // topLeft (far) --- bottomLeft (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, bl_dst)) { far1_src = tl; far2_src = tr; near1_src = bl; near2_src = br; far1_dst = tl_dst; far2_dst = tr_dst; near1_dst = bl_dst; near2_dst = br_dst; // bottomLeft (far) --- topLeft (near) --- vanishing } else { far1_src = bl; far2_src = br; near1_src = tl; near2_src = tr; far1_dst = bl_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = tr_dst; } } QLineF l0(far1_dst, mousePos); QLineF l1(far2_dst, mousePos); QLineF l2(otherV_dst, near1_dst); l0.intersect(l2, &near1_dst); l1.intersect(l2, &near2_dst); srcPoints << far1_src; srcPoints << far2_src; srcPoints << near1_src; srcPoints << near2_src; dstPoints << far1_dst; dstPoints << far2_dst; dstPoints << near1_dst; dstPoints << near2_dst; Eigen::Matrix3f A = getTransitionMatrix(srcPoints); Eigen::Matrix3f B = getTransitionMatrix(dstPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } } m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; + m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisPerspectiveTransformStrategy::Private::recalculateTransformations() { transform = transformFromArgs(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not const qreal maxScale = 20.0; imageTooBig = false; if (qAbs(currentArgs.scaleX()) > maxScale || qAbs(currentArgs.scaleY()) > maxScale) { imageTooBig = true; } else { QVector points; points << transaction.originalRect().topLeft(); points << transaction.originalRect().topRight(); points << transaction.originalRect().bottomRight(); points << transaction.originalRect().bottomLeft(); for (int i = 0; i < points.size(); i++) { points[i] = transform.map(points[i]); } for (int i = 0; i < points.size(); i++) { const QPointF &pt = points[i]; const QPointF &prev = points[(i - 1 + 4) % 4]; const QPointF &next = points[(i + 1) % 4]; const QPointF &other = points[(i + 2) % 4]; QLineF l1(pt, other); QLineF l2(prev, next); QPointF intersection; l1.intersect(l2, &intersection); qreal maxDistance = kisSquareDistance(pt, other); if (kisSquareDistance(pt, intersection) > maxDistance || kisSquareDistance(other, intersection) > maxDistance) { imageTooBig = true; break; } const qreal thresholdDistance = 0.02 * l2.length(); if (kisDistanceToLine(pt, l2) < thresholdDistance) { imageTooBig = true; break; } } } // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); }