WIP: unify project, product and plugin class spelling; migration pending
这个提交包含在:
1 父节点
d1137edd83
当前提交
47f6e55ecd
40 个文件已更改
+138
-116
没有与搜索条件相匹配的文件
@@ -0,0 +1,562 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 110;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6945FC8F3088B7220013E108 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FC8E3088B7220013E108 /* Cocoa.framework */; };
|
||||
6945FC973088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit in Embed PlugIns */ = {isa = PBXBuildFile; fileRef = 6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
6945FC9B3088B7220013E108 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FC9A3088B7220013E108 /* Foundation.framework */; };
|
||||
6945FC9D3088B7220013E108 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FC9C3088B7220013E108 /* AppKit.framework */; };
|
||||
6945FC9F3088B7220013E108 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FC9E3088B7220013E108 /* Accelerate.framework */; };
|
||||
6945FCA13088B7220013E108 /* PluginManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FCA03088B7220013E108 /* PluginManager.framework */; };
|
||||
6945FCA33088B7220013E108 /* FxPlug.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6945FCA23088B7220013E108 /* FxPlug.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
6945FC983088B7220013E108 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 6945FC833088B7220013E108 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 6945FC953088B7220013E108;
|
||||
remoteInfo = "XPC Service";
|
||||
};
|
||||
6945FCA43088B7220013E108 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 6945FC833088B7220013E108 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 6945FC953088B7220013E108;
|
||||
remoteInfo = "XPC Service";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
6945FCCF3088B7220013E108 /* Embed PlugIns */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
dstPath = "";
|
||||
dstSubfolder = PlugIns;
|
||||
files = (
|
||||
6945FC973088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit in Embed PlugIns */,
|
||||
);
|
||||
name = "Embed PlugIns";
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
6945FC8B3088B7220013E108 /* FCP-BeautyGirl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FCP-BeautyGirl.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6945FC8E3088B7220013E108 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
|
||||
6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FCP-BeautyGirl XPC Service.pluginkit"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6945FC9A3088B7220013E108 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||
6945FC9C3088B7220013E108 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
|
||||
6945FC9E3088B7220013E108 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
|
||||
6945FCA03088B7220013E108 /* PluginManager.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PluginManager.framework; path = Library/Frameworks/../../../../../../Library/Developer/Frameworks/PluginManager.framework; sourceTree = DEVELOPER_DIR; };
|
||||
6945FCA23088B7220013E108 /* FxPlug.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FxPlug.framework; path = Library/Frameworks/../../../../../../Library/Developer/Frameworks/FxPlug.framework; sourceTree = DEVELOPER_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
6945FCCB3088B7220013E108 /* Exceptions for "FCP-BeautyGirl" folder in "Wrapper Application" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"/Localized: Plugin/InfoPlist.strings",
|
||||
"/Localized: Plugin/Localizable.strings",
|
||||
Plugin/FCPBeautyGirlPlugIn.m,
|
||||
Plugin/Info.plist,
|
||||
Plugin/main.m,
|
||||
Plugin/PGBeautyRenderer.m,
|
||||
Plugin/PGFaceProcessor.m,
|
||||
Plugin/PGFaceSelectOSC.m,
|
||||
Plugin/PGInspectorState.m,
|
||||
Plugin/PGInspectorView.m,
|
||||
"Wrapper Application/Info.plist",
|
||||
);
|
||||
target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
|
||||
};
|
||||
6945FCD33088B7220013E108 /* Exceptions for "FCP-BeautyGirl" folder in "XPC Service" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"/Localized: Wrapper Application/Credits.rtf",
|
||||
"/Localized: Wrapper Application/InfoPlist.strings",
|
||||
"/Localized: Wrapper Application/MainMenu.xib",
|
||||
Plugin/Info.plist,
|
||||
"Wrapper Application/AppDelegate.m",
|
||||
"Wrapper Application/Resources/LauncherInfo.png",
|
||||
"Wrapper Application/Info.plist",
|
||||
"Wrapper Application/main.m",
|
||||
);
|
||||
target = 6945FC953088B7220013E108 /* XPC Service */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
6945FCA63088B7220013E108 /* FCP-BeautyGirl */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
6945FCCB3088B7220013E108 /* Exceptions for "FCP-BeautyGirl" folder in "Wrapper Application" target */,
|
||||
6945FCD33088B7220013E108 /* Exceptions for "FCP-BeautyGirl" folder in "XPC Service" target */,
|
||||
);
|
||||
path = FCP-BeautyGirl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
6945FC883088B7220013E108 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
files = (
|
||||
6945FC8F3088B7220013E108 /* Cocoa.framework in Frameworks */,
|
||||
);
|
||||
};
|
||||
6945FC913088B7220013E108 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
files = (
|
||||
6945FC9F3088B7220013E108 /* Accelerate.framework in Frameworks */,
|
||||
6945FCA13088B7220013E108 /* PluginManager.framework in Frameworks */,
|
||||
6945FC9B3088B7220013E108 /* Foundation.framework in Frameworks */,
|
||||
6945FC9D3088B7220013E108 /* AppKit.framework in Frameworks */,
|
||||
6945FCA33088B7220013E108 /* FxPlug.framework in Frameworks */,
|
||||
);
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
6945FC823088B7220013E108 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6945FCA63088B7220013E108 /* FCP-BeautyGirl */,
|
||||
6945FC8D3088B7220013E108 /* Frameworks */,
|
||||
6945FC8C3088B7220013E108 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6945FC8C3088B7220013E108 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6945FC8B3088B7220013E108 /* FCP-BeautyGirl.app */,
|
||||
6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6945FC8D3088B7220013E108 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6945FC8E3088B7220013E108 /* Cocoa.framework */,
|
||||
6945FC9A3088B7220013E108 /* Foundation.framework */,
|
||||
6945FC9C3088B7220013E108 /* AppKit.framework */,
|
||||
6945FC9E3088B7220013E108 /* Accelerate.framework */,
|
||||
6945FCA03088B7220013E108 /* PluginManager.framework */,
|
||||
6945FCA23088B7220013E108 /* FxPlug.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
6945FC8A3088B7220013E108 /* Wrapper Application */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6945FCD03088B7220013E108 /* Build configuration list for PBXNativeTarget "Wrapper Application" */;
|
||||
buildPhases = (
|
||||
6945FC873088B7220013E108 /* Sources */,
|
||||
6945FC883088B7220013E108 /* Frameworks */,
|
||||
6945FC893088B7220013E108 /* Resources */,
|
||||
6945FCCF3088B7220013E108 /* Embed PlugIns */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
6945FC993088B7220013E108 /* PBXTargetDependency */,
|
||||
6945FCA53088B7220013E108 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
6945FCA63088B7220013E108 /* FCP-BeautyGirl */,
|
||||
);
|
||||
name = "Wrapper Application";
|
||||
productName = "Wrapper Application";
|
||||
productReference = 6945FC8B3088B7220013E108 /* FCP-BeautyGirl.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
6945FC953088B7220013E108 /* XPC Service */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6945FCCC3088B7220013E108 /* Build configuration list for PBXNativeTarget "XPC Service" */;
|
||||
buildPhases = (
|
||||
6945FC903088B7220013E108 /* Sources */,
|
||||
6945FC913088B7220013E108 /* Frameworks */,
|
||||
6945FC923088B7220013E108 /* Resources */,
|
||||
6945FC933088B7220013E108 /* Copy and Code Sign FxPlug.framework */,
|
||||
6945FC943088B7220013E108 /* Copy and Code Sign PluginManager.framework */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
6945FCA63088B7220013E108 /* FCP-BeautyGirl */,
|
||||
);
|
||||
name = "XPC Service";
|
||||
productName = "XPC Service";
|
||||
productReference = 6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
6945FC833088B7220013E108 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastUpgradeCheck = 2710;
|
||||
TargetAttributes = {
|
||||
6945FC8A3088B7220013E108 = {
|
||||
CreatedOnToolsVersion = 27.1;
|
||||
};
|
||||
6945FC953088B7220013E108 = {
|
||||
CreatedOnToolsVersion = 27.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 6945FC863088B7220013E108 /* Build configuration list for PBXProject "FCP-BeautyGirl" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 6945FC823088B7220013E108;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 6945FC8C3088B7220013E108 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
6945FC8A3088B7220013E108 /* Wrapper Application */,
|
||||
6945FC953088B7220013E108 /* XPC Service */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
6945FC893088B7220013E108 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
files = (
|
||||
);
|
||||
};
|
||||
6945FC923088B7220013E108 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
files = (
|
||||
);
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6945FC933088B7220013E108 /* Copy and Code Sign FxPlug.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign FxPlug.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
"mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/FxPlug.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework/\"",
|
||||
"if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed FxPlug.framework\"; exit 1; fi",
|
||||
"codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework\"",
|
||||
);
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6945FC943088B7220013E108 /* Copy and Code Sign PluginManager.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign PluginManager.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
"mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/PluginManager.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework/\"",
|
||||
"if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed PluginManager.framework\"; exit 1; fi",
|
||||
"codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework\"",
|
||||
"",
|
||||
);
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
6945FC873088B7220013E108 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
files = (
|
||||
);
|
||||
};
|
||||
6945FC903088B7220013E108 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
files = (
|
||||
);
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
6945FC993088B7220013E108 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 6945FC953088B7220013E108 /* XPC Service */;
|
||||
targetProxy = 6945FC983088B7220013E108 /* PBXContainerItemProxy */;
|
||||
};
|
||||
6945FCA53088B7220013E108 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 6945FC953088B7220013E108 /* XPC Service */;
|
||||
targetProxy = 6945FCA43088B7220013E108 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = FCP-BeautyGirl/Plugin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = mh_execute;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug;
|
||||
PRODUCT_NAME = "FCP-BeautyGirl $(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h";
|
||||
WRAPPER_EXTENSION = pluginkit;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = FCP-BeautyGirl/Plugin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = mh_execute;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug;
|
||||
PRODUCT_NAME = "FCP-BeautyGirl $(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h";
|
||||
WRAPPER_EXTENSION = pluginkit;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6945FCD13088B7220013E108 /* Debug configuration for PBXNativeTarget "Wrapper Application" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "FCP-BeautyGirl/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-framework", CoreServices, );
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = "FCP-BeautyGirl/Wrapper Application/Info.plist";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl;
|
||||
PRODUCT_NAME = FCP-BeautyGirl;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Wrapper Application/Wrapper Application-Bridging-Header.h";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6945FCD23088B7220013E108 /* Release configuration for PBXNativeTarget "Wrapper Application" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "FCP-BeautyGirl/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-framework", CoreServices, );
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = "FCP-BeautyGirl/Wrapper Application/Info.plist";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl;
|
||||
PRODUCT_NAME = FCP-BeautyGirl;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Wrapper Application/Wrapper Application-Bridging-Header.h";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6945FCD43088B7220013E108 /* Debug configuration for PBXProject "FCP-BeautyGirl" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
x86_64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks,
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6945FCD53088B7220013E108 /* Release configuration for PBXProject "FCP-BeautyGirl" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
x86_64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks,
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
6945FC863088B7220013E108 /* Build configuration list for PBXProject "FCP-BeautyGirl" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6945FCD43088B7220013E108 /* Debug configuration for PBXProject "FCP-BeautyGirl" */,
|
||||
6945FCD53088B7220013E108 /* Release configuration for PBXProject "FCP-BeautyGirl" */,
|
||||
);
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
6945FCCC3088B7220013E108 /* Build configuration list for PBXNativeTarget "XPC Service" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */,
|
||||
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */,
|
||||
);
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
6945FCD03088B7220013E108 /* Build configuration list for PBXNativeTarget "Wrapper Application" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6945FCD13088B7220013E108 /* Debug configuration for PBXNativeTarget "Wrapper Application" */,
|
||||
6945FCD23088B7220013E108 /* Release configuration for PBXNativeTarget "Wrapper Application" */,
|
||||
);
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 6945FC833088B7220013E108 /* Project object */;
|
||||
validationLevel = 1;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,11 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
@interface FCPBeautyGirlPlugIn : NSObject <FxTileableEffect, FxAnalyzer, FxCustomParameterViewHost_v2>
|
||||
@property (nonatomic, assign) id<PROAPIAccessing> apiManager;
|
||||
- (PGBeautySettings)settingsAtTime:(CMTime)time;
|
||||
- (void)analyzeForward;
|
||||
- (void)analyzeBackward;
|
||||
- (void)clearTracking;
|
||||
@end
|
||||
@@ -0,0 +1,398 @@
|
||||
#import "FCPBeautyGirlPlugIn.h"
|
||||
#import "PGParameterIDs.h"
|
||||
#import "PGBeautyRenderer.h"
|
||||
#import "PGFaceProcessor.h"
|
||||
#import "PGInspectorState.h"
|
||||
#import "PGInspectorView.h"
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import <stddef.h>
|
||||
#import <math.h>
|
||||
#import <os/log.h>
|
||||
|
||||
static os_log_t PGRenderLog(void) {
|
||||
static os_log_t log; static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{ log=os_log_create("com.dayu.fcpbeautygirl", "render"); });
|
||||
return log;
|
||||
}
|
||||
|
||||
typedef struct { UInt32 identifier; const char *name; size_t offset; float min, max, step; BOOL percent; } PGSlider;
|
||||
#define PGS(ID, NAME, FIELD, MIN, MAX, STEP, PERCENT) {ID, NAME, offsetof(PGBeautySettings, FIELD), MIN, MAX, STEP, PERCENT}
|
||||
static const PGSlider PGSliders[] = {
|
||||
PGS(PGAmount,"磨皮强度",amount,0,1,.01,YES),
|
||||
PGS(PGMidtone,"中频磨皮",midtoneDetail,0,1,.01,YES),
|
||||
PGS(PGRadius,"磨皮半径",radius,1,30,.5,NO),
|
||||
PGS(PGBrightness,"肤色提亮",skinBrightness,0,1,.01,YES),
|
||||
PGS(PGHue,"肤色色相",skinHue,-30,30,1,NO),
|
||||
PGS(PGSaturation,"肤色饱和度",skinSaturation,-100,100,1,NO),
|
||||
PGS(PGEvenTone,"均匀肤色",skinEvenTone,0,1,.01,YES),
|
||||
PGS(PGTeeth,"牙齿美白",teethWhitening,0,1,.01,YES),
|
||||
PGS(PGEyeBright,"眼睛提亮",eyeBrightening,0,1,.01,YES),
|
||||
PGS(PGHighPass,"高频细节平滑",highPassDetail,0,1,.01,YES),
|
||||
PGS(PGSharpness,"锐化",sharpness,0,1,.01,YES),
|
||||
PGS(PGFeatureProtectionAmount,"五官保护强度",featureProtectionAmount,0,1,.01,YES),
|
||||
PGS(PGNonSkinProtection,"非肤色区保护",nonSkinProtection,0,1,.01,YES),
|
||||
PGS(PGMicroDetail,"微细锐化",microDetail,0,1,.01,YES),
|
||||
PGS(PGThin,"瘦脸",faceThin,0,1,.01,YES),
|
||||
PGS(PGBigEye,"大眼",faceBigEye,0,1,.01,YES),
|
||||
PGS(PGShort,"短脸",faceShort,-1,1,.01,NO),
|
||||
PGS(PGNarrow,"窄脸",faceNarrow,-1,1,.01,NO),
|
||||
PGS(PGChin,"下巴",faceChin,-1,1,.01,NO),
|
||||
PGS(PGVFace,"V 脸",faceV,0,1,.01,YES),
|
||||
PGS(PGJaw,"下颌",faceJaw,0,1,.01,YES),
|
||||
PGS(PGCheekbone,"颧骨",faceCheekbone,0,1,.01,YES),
|
||||
PGS(PGSmallHead,"小头",faceSmallHead,0,1,.01,YES),
|
||||
PGS(PGSmallHeadRange,"小头范围",faceSmallHeadRange,0,1,.01,YES),
|
||||
PGS(PGThreshold,"蒙版阈值",maskThreshold,0,255,1,NO),
|
||||
PGS(PGSoftness,"蒙版柔度",maskSoftness,0,1,.01,YES),
|
||||
PGS(PGEdgeProtect,"边缘保护",maskEdgeProtect,0,1,.01,YES),
|
||||
PGS(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,1,4,.05,NO),
|
||||
PGS(PGMix,"整体混合",mix,0,1,.01,YES),
|
||||
};
|
||||
#undef PGS
|
||||
|
||||
static BOOL PGFail(NSError **error, NSString *message) {
|
||||
os_log_error(PGRenderLog(), "%{public}@", message);
|
||||
if (error) *error = [NSError errorWithDomain:FxPlugErrorDomain code:kFxError_InvalidParameter
|
||||
userInfo:@{NSLocalizedDescriptionKey:message}];
|
||||
return NO;
|
||||
}
|
||||
static NSString * const PGHostCoordinateSpace = @"iosurface-ci-y-up-v1";
|
||||
static BOOL PGCompatibleHostTrack(NSData *data) {
|
||||
if (!data.length || data.length > 128*1024*1024) return NO;
|
||||
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
|
||||
return [archive isKindOfClass:NSDictionary.class] && [archive[@"hostCoordinateSpace"] isEqual:PGHostCoordinateSpace];
|
||||
}
|
||||
static NSData *PGMarkHostTrack(NSData *data) {
|
||||
if (!data) return nil;
|
||||
NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainers format:nil error:nil];
|
||||
if (![archive isKindOfClass:NSMutableDictionary.class]) return nil;
|
||||
archive[@"hostCoordinateSpace"] = PGHostCoordinateSpace;
|
||||
return [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
if (!tile.ioSurface) return nil;
|
||||
// The host already supplies video-gamma RGB. Keep those numeric values in
|
||||
// every stage; implicit Core Image linearization changes mask thresholds.
|
||||
NSDictionary *options = @{kCIImageColorSpace:NSNull.null};
|
||||
CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options];
|
||||
// Match the reference's CI/IOSurface path. imageOrigin describes the host's
|
||||
// coordinate axes; it is not an instruction to reverse IOSurface scanlines.
|
||||
// An extra input/output flip can hide in passthrough while inverting faces
|
||||
// inside Vision and the analytic shape/mask stages.
|
||||
FxRect b = tile.tilePixelBounds;
|
||||
return [image imageByApplyingTransform:CGAffineTransformMakeTranslation(b.left,b.bottom)];
|
||||
}
|
||||
|
||||
@interface FCPBeautyGirlPlugIn ()
|
||||
@property (strong) PGBeautyRenderer *beautyRenderer;
|
||||
@property (strong) PGFaceProcessor *faceProcessor;
|
||||
@property (strong) PGFaceProcessor *analysisFaceProcessor;
|
||||
@property (strong) NSMutableDictionary<NSNumber *,CIContext *> *contexts;
|
||||
@property PGBeautySettings analysisSettings;
|
||||
@property CMTime analysisTime;
|
||||
@property NSUInteger analyzedFrames;
|
||||
@property BOOL analyzing;
|
||||
@property BOOL analysisForward;
|
||||
@property (strong) NSArray<NSString *> *loadedTracks;
|
||||
@property BOOL reportedRender;
|
||||
@property (strong) NSHashTable<PGInspectorView *> *inspectorViews;
|
||||
@end
|
||||
|
||||
@implementation FCPBeautyGirlPlugIn
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
|
||||
if ((self=[super init])) {
|
||||
_apiManager=manager;
|
||||
_beautyRenderer=[PGBeautyRenderer new]; _faceProcessor=[PGFaceProcessor new];
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
_contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults();
|
||||
_analysisTime=kCMTimeZero;
|
||||
_inspectorViews=[NSHashTable weakObjectsHashTable];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (BOOL)properties:(NSDictionary **)properties error:(NSError **)error {
|
||||
if (!properties) return PGFail(error,@"宿主未提供属性输出位置。");
|
||||
*properties=@{kFxPropertyKey_MayRemapTime:@NO,
|
||||
kFxPropertyKey_PixelTransformSupport:@(kFxPixelTransform_ScaleTranslate),
|
||||
kFxPropertyKey_VariesWhenParamsAreStatic:@NO,
|
||||
@"NeedsFullBuffer":@YES, @"ChangesOutputSize":@NO,
|
||||
kFxPropertyKey_DesiredProcessingColorInfo:@(kFxImageColorInfo_RGB_GAMMA_VIDEO)};
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)addSlider:(UInt32)identifier api:(id<FxParameterCreationAPI_v5>)api {
|
||||
PGBeautySettings defaults=PGBeautyDefaults();
|
||||
for (NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
|
||||
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
|
||||
NSString *name=[NSString stringWithUTF8String:spec.name];
|
||||
float value=*(float *)((uint8_t *)&defaults+spec.offset);
|
||||
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
|
||||
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
- (BOOL)addParametersWithError:(NSError **)error {
|
||||
id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
|
||||
if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。");
|
||||
__block BOOL ok=YES;
|
||||
ok=[api addCustomParameterWithName:@"" parameterID:PGInspectorPanel
|
||||
defaultValue:[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO]
|
||||
parameterFlags:kFxParameterFlag_CUSTOM_UI|kFxParameterFlag_USE_FULL_VIEW_WIDTH]&&ok;
|
||||
void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;};
|
||||
void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
|
||||
void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
|
||||
void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;};
|
||||
group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES);
|
||||
slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end();
|
||||
group(@"磨皮保护与细节",106); slider(PGFeatureProtectionAmount); slider(PGNonSkinProtection); slider(PGMicroDetail); end();
|
||||
group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES);
|
||||
ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||
slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end();
|
||||
group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end();
|
||||
group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES);
|
||||
UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange};
|
||||
for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
|
||||
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
|
||||
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
|
||||
toggle(@"使用跟踪数据",PGTrackEnabled,NO);
|
||||
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_HIDDEN]&&ok;
|
||||
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"自动人脸跟随" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED|kFxParameterFlag_HIDDEN]&&ok;
|
||||
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
|
||||
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
|
||||
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
|
||||
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||
slider(PGMix); end();
|
||||
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
|
||||
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
||||
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
|
||||
}
|
||||
- (NSView *)createViewForParameterID:(UInt32)identifier {
|
||||
if(identifier!=PGInspectorPanel)return nil;
|
||||
PGInspectorView *view=[[PGInspectorView alloc] initWithAPIManager:_apiManager];
|
||||
@synchronized(_inspectorViews){[_inspectorViews addObject:view];}return view;
|
||||
}
|
||||
- (BOOL)parameterChanged:(UInt32)identifier atTime:(CMTime)time error:(NSError **)error {
|
||||
// The host callback covers undo and playhead-driven parameter changes. Hop
|
||||
// to AppKit's thread and avoid retaining remote inspector views in render.
|
||||
NSArray *views;@synchronized(_inspectorViews){views=_inspectorViews.allObjects;}
|
||||
for(PGInspectorView *view in views)dispatch_async(dispatch_get_main_queue(), ^{[view hostParameterChanged];});
|
||||
return YES;
|
||||
}
|
||||
- (Class)classForCustomParameterID:(UInt32)identifier {return identifier==PGInspectorPanel?PGInspectorState.class:Nil;}
|
||||
- (NSSet<Class> *)classesForCustomParameterID:(UInt32)identifier {
|
||||
return identifier==PGInspectorPanel?[NSSet setWithObjects:PGInspectorState.class,NSDictionary.class,NSNumber.class,NSSet.class,nil]:[NSSet set];
|
||||
}
|
||||
- (PGBeautySettings)settingsAtTime:(CMTime)time {
|
||||
PGBeautySettings s=PGBeautyDefaults();
|
||||
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
if(!api) return s;
|
||||
for(NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
|
||||
PGSlider spec=PGSliders[i]; double value=*(float *)((uint8_t *)&s+spec.offset);
|
||||
if([api getFloatValue:&value fromParameter:spec.identifier atTime:time] && isfinite(value))
|
||||
*(float *)((uint8_t *)&s+spec.offset)=(float)fmin(spec.max,fmax(spec.min,value));
|
||||
}
|
||||
struct {UInt32 identifier;size_t offset;} toggles[]={
|
||||
{PGFeatureProtect,offsetof(PGBeautySettings,featureProtection)}, {PGShowMask,offsetof(PGBeautySettings,showMask)},
|
||||
{PGInvertMask,offsetof(PGBeautySettings,invertMask)}, {PGFaceBoxes,offsetof(PGBeautySettings,showFaceBoxes)},
|
||||
{PGTrackBoxes,offsetof(PGBeautySettings,showTrackBox)}, {PGSingleFace,offsetof(PGBeautySettings,singleFace)},
|
||||
{PGFastMulti,offsetof(PGBeautySettings,fastMultiFace)}, {PGTrackEnabled,offsetof(PGBeautySettings,trackingEnabled)}};
|
||||
for(NSUInteger i=0;i<sizeof(toggles)/sizeof(toggles[0]);i++) {
|
||||
BOOL value=*(uint32_t *)((uint8_t *)&s+toggles[i].offset)!=0;
|
||||
[api getBoolValue:&value fromParameter:toggles[i].identifier atTime:time];
|
||||
*(uint32_t *)((uint8_t *)&s+toggles[i].offset)=value;
|
||||
}
|
||||
int value=0; [api getIntValue:&value fromParameter:PGTonePreset atTime:time]; s.skinTonePreset=MIN(6,MAX(0,value));
|
||||
value=0; [api getIntValue:&value fromParameter:PGDebugChannel atTime:time]; s.debugChannel=MIN(2,MAX(0,value));
|
||||
value=0; [api getIntValue:&value fromParameter:PGTarget atTime:time]; s.targetIndex=MIN(2,MAX(0,value));
|
||||
double x=.5,y=.5; [api getXValue:&x YValue:&y fromParameter:PGSelection atTime:time];
|
||||
s.selectionX=isfinite(x)?fmin(1,fmax(0,x)):.5; s.selectionY=isfinite(y)?fmin(1,fmax(0,y)):.5;
|
||||
BOOL enabled=YES;
|
||||
[api getBoolValue:&enabled fromParameter:PGSmoothingEnabled atTime:time]; if(!enabled) s.amount=s.midtoneDetail=s.microDetail=0;
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGColorEnabled atTime:time];
|
||||
if(!enabled) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGDetailEnabled atTime:time]; if(!enabled) s.highPassDetail=s.sharpness=0;
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
|
||||
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
||||
if(s.fastMultiFace) s.singleFace=0;
|
||||
NSObject *panel=nil;
|
||||
[api getCustomParameterValue:&panel fromParameter:PGInspectorPanel atTime:time];
|
||||
if([panel isKindOfClass:PGInspectorState.class])s=PGSettingsWithInspectorState(s,(PGInspectorState *)panel);
|
||||
// The current product follows faces automatically. Old manual-analysis and
|
||||
// debug values remain saved for document compatibility, but cannot silently
|
||||
// override live geometry or leave an invisible mask/selection mode active.
|
||||
s.trackingEnabled=0;s.singleFace=0;s.showMask=0;s.invertMask=0;s.debugChannel=0;s.showFaceBoxes=0;
|
||||
return s;
|
||||
}
|
||||
- (NSArray<NSString *> *)trackStrings {
|
||||
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
NSMutableArray *tracks=[NSMutableArray new];
|
||||
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++) {
|
||||
NSString *value=@"";[api getStringParameterValue:&value fromParameter:identifier];
|
||||
[tracks addObject:value?:@""];
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
- (BOOL)pluginState:(NSData **)state atTime:(CMTime)time quality:(FxQuality)quality error:(NSError **)error {
|
||||
if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
|
||||
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
|
||||
PGBeautySettings s=[self settingsAtTime:time];
|
||||
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":@[@"",@"",@""]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
|
||||
return *state!=nil;
|
||||
}
|
||||
- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(!rect||sources.count==0) return PGFail(error,@"缺少输入图像。");
|
||||
*rect=sources.firstObject.imagePixelBounds;return YES;
|
||||
}
|
||||
- (BOOL)sourceTileRect:(FxRect *)rect sourceImageIndex:(NSUInteger)index sourceImages:(NSArray<FxImageTile *> *)sources destinationTileRect:(FxRect)destinationRect destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(!rect||index>=sources.count) return PGFail(error,@"输入图像索引无效。");
|
||||
*rect=sources[index].imagePixelBounds;return YES;
|
||||
}
|
||||
- (CIContext *)contextForTile:(FxImageTile *)tile {
|
||||
NSNumber *key=@(tile.deviceRegistryID);
|
||||
@synchronized(_contexts) {
|
||||
if(!_contexts[key]) {
|
||||
id<MTLDevice> device=nil;
|
||||
for(id<MTLDevice> candidate in MTLCopyAllDevices()) if(candidate.registryID==tile.deviceRegistryID) {device=candidate;break;}
|
||||
if(!device) device=MTLCreateSystemDefaultDevice();
|
||||
if(!device) return nil;
|
||||
_contexts[key]=[CIContext contextWithMTLDevice:device options:@{kCIContextCacheIntermediates:@NO,kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
}
|
||||
return _contexts[key];
|
||||
}
|
||||
}
|
||||
- (BOOL)renderDestinationImage:(FxImageTile *)destination sourceImages:(NSArray<FxImageTile *> *)sources pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(sources.count==0||!sources.firstObject.ioSurface||!destination.ioSurface||![state isKindOfClass:NSData.class]||state.length==0||state.length>320*1024*1024) return PGFail(error,@"宿主传入的画面或状态无效。");
|
||||
NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:state options:NSPropertyListImmutable format:nil error:error];
|
||||
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]) return PGFail(error,@"渲染状态版本无效。");
|
||||
NSData *encoded=plist[@"settings"]; uint32_t version=0;
|
||||
if(encoded.length<sizeof(version)) return PGFail(error,@"渲染状态版本无效。");
|
||||
[encoded getBytes:&version length:sizeof(version)];
|
||||
PGBeautySettings s=PGBeautyDefaults();
|
||||
if(version==1 && encoded.length==PGBeautySettingsV1Size) {
|
||||
[encoded getBytes:&s length:PGBeautySettingsV1Size];s.version=PGBeautySettingsVersion;
|
||||
} else if(version==PGBeautySettingsVersion && encoded.length==sizeof(s)) {
|
||||
[encoded getBytes:&s length:sizeof(s)];
|
||||
} else return PGFail(error,@"此版本不支持保存的渲染状态。");
|
||||
s.trackingEnabled=0;s.singleFace=0;
|
||||
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
|
||||
@synchronized(self) {
|
||||
if(!_reportedRender) {
|
||||
_reportedRender=YES;
|
||||
IOSurfaceRef src=(__bridge IOSurfaceRef)sources.firstObject.ioSurface;
|
||||
IOSurfaceRef dst=(__bridge IOSurfaceRef)destination.ioSurface;
|
||||
FxRect a=sources.firstObject.tilePixelBounds,b=destination.tilePixelBounds;
|
||||
os_log(PGRenderLog(), "source format=%u %zux%zu bytes=%zu planes=%zu origin=%lu bounds=%d,%d,%d,%d extent=%{public}@; destination format=%u %zux%zu bytes=%zu planes=%zu origin=%lu bounds=%d,%d,%d,%d; midtone=%f mix=%f mask=%u",
|
||||
IOSurfaceGetPixelFormat(src),IOSurfaceGetWidth(src),IOSurfaceGetHeight(src),IOSurfaceGetBytesPerElement(src),IOSurfaceGetPlaneCount(src),(unsigned long)sources.firstObject.imageOrigin,a.left,a.bottom,a.right,a.top,NSStringFromRect(input.extent),
|
||||
IOSurfaceGetPixelFormat(dst),IOSurfaceGetWidth(dst),IOSurfaceGetHeight(dst),IOSurfaceGetBytesPerElement(dst),IOSurfaceGetPlaneCount(dst),(unsigned long)destination.imageOrigin,b.left,b.bottom,b.right,b.top,s.midtoneDetail,s.mix,s.showMask);
|
||||
}
|
||||
}
|
||||
CIContext *context=[self contextForTile:destination]; if(!context) return PGFail(error,@"没有可用的 Metal 图形设备。");
|
||||
CIImage *output=nil; CIImage *overlay=nil;
|
||||
@synchronized(_faceProcessor) {
|
||||
NSArray *tracks=plist[@"tracks"];
|
||||
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
|
||||
for(id track in tracks) if(![track isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
|
||||
NSError *stageError=nil;
|
||||
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
|
||||
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
||||
PGBeautySettings beauty=s; beauty.mix=1;
|
||||
output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
|
||||
if(!output) { os_log_error(PGRenderLog(), "beauty processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
||||
overlay=frame.overlayImage;
|
||||
}
|
||||
if(!output) return NO;
|
||||
// Blend from the original frame so zero mix also disables geometry.
|
||||
if(s.mix<1 && !s.showMask) {
|
||||
output=[output imageByApplyingFilter:@"CIDissolveTransition" withInputParameters:@{kCIInputTargetImageKey:input,kCIInputTimeKey:@(1-s.mix)}];
|
||||
}
|
||||
if(overlay && !s.showMask) output=[overlay imageByCompositingOverImage:output];
|
||||
FxRect b=destination.tilePixelBounds;
|
||||
output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)];
|
||||
size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface);
|
||||
@try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:nil];}
|
||||
@catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");}
|
||||
return YES;
|
||||
}
|
||||
- (void)setStatus:(NSString *)status {
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:status toParameter:PGStatus];
|
||||
}
|
||||
- (CMTime)currentHostTime {
|
||||
id<FxCustomParameterActionAPI_v4> api=[_apiManager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
|
||||
return api ? [api currentTime] : kCMTimeZero;
|
||||
}
|
||||
- (void)startAnalysisForward:(BOOL)forward {
|
||||
if(_analyzing) {[self setStatus:@"请先等待当前分析结束或取消分析。"];return;}
|
||||
NSError *error=nil;
|
||||
id<FxAnalysisAPI> api=[_apiManager apiForProtocol:@protocol(FxAnalysisAPI)];
|
||||
if(!api) {[self setStatus:@"当前宿主不提供帧分析接口。"];return;}
|
||||
_analysisTime=[self currentHostTime];
|
||||
PGBeautySettings s=[self settingsAtTime:_analysisTime];
|
||||
NSArray<NSString *> *tracks=[self trackStrings];
|
||||
// A separate processor owns the mutable analysis session. Render snapshots
|
||||
// cannot overwrite records being accumulated by the analyzer.
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
NSData *previous=[[NSData alloc] initWithBase64EncodedString:tracks[s.targetIndex] options:0];
|
||||
[_analysisFaceProcessor setTrackingData:PGCompatibleHostTrack(previous)?previous:nil forTarget:s.targetIndex];
|
||||
_analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1;
|
||||
_analysisForward=forward;_analyzing=YES;
|
||||
BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error];
|
||||
if(!ok) _analyzing=NO;
|
||||
[self setStatus:ok?@"正在分析所选人脸…":(error.localizedDescription?:@"宿主未能开始帧分析。")];
|
||||
}
|
||||
- (void)analyzeForward {[self startAnalysisForward:YES];}
|
||||
- (void)analyzeBackward {[self startAnalysisForward:NO];}
|
||||
- (void)clearTracking {
|
||||
if(_analyzing) {[self setStatus:@"请先结束或取消分析,再清除跟踪数据。"];return;}
|
||||
PGBeautySettings s=[self settingsAtTime:[self currentHostTime]];
|
||||
@synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];_loadedTracks=nil;}
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:@"" toParameter:PGTrackData1+s.targetIndex];
|
||||
[self setStatus:@"已清除当前目标的分析数据"];
|
||||
}
|
||||
- (BOOL)desiredAnalysisTimeRange:(CMTimeRange *)desired forInputWithTimeRange:(CMTimeRange)range error:(NSError **)error {
|
||||
if(!desired) return PGFail(error,@"缺少分析时间范围。");
|
||||
*desired=range;
|
||||
if(CMTIMERANGE_IS_VALID(range) && CMTIME_IS_NUMERIC(_analysisTime) && CMTimeRangeContainsTime(range,_analysisTime)) {
|
||||
if(_analysisForward) *desired=CMTimeRangeFromTimeToTime(_analysisTime,CMTimeRangeGetEnd(range));
|
||||
else {
|
||||
CMTime last=CMTimeAdd(_analysisTime,CMTimeMake(1,MAX(1,_analysisTime.timescale)));
|
||||
if(CMTimeCompare(last,CMTimeRangeGetEnd(range))>0) last=CMTimeRangeGetEnd(range);
|
||||
*desired=CMTimeRangeFromTimeToTime(range.start,last);
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)setupAnalysisForTimeRange:(CMTimeRange)range frameDuration:(CMTime)duration error:(NSError **)error {
|
||||
_analyzedFrames=0;_analyzing=YES;return YES;
|
||||
}
|
||||
- (BOOL)analyzeFrame:(FxImageTile *)frame atTime:(CMTime)time error:(NSError **)error {
|
||||
CIImage *image=PGImageFromTile(frame);if(!image) return PGFail(error,@"无法读取分析帧。");
|
||||
BOOL ok=NO;
|
||||
@synchronized(_analysisFaceProcessor) {ok=[_analysisFaceProcessor analyzeImage:image settings:_analysisSettings time:time error:error];}
|
||||
if(ok) _analyzedFrames++;
|
||||
return ok;
|
||||
}
|
||||
- (BOOL)cleanupAnalysis:(NSError **)error {
|
||||
_analyzing=NO;
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
NSData *data=nil;
|
||||
@synchronized(_analysisFaceProcessor) {data=[_analysisFaceProcessor trackingDataForTarget:_analysisSettings.targetIndex];}
|
||||
data=PGMarkHostTrack(data);
|
||||
@synchronized(_faceProcessor) {_loadedTracks=nil;}
|
||||
if(data.length>64*1024*1024) {
|
||||
_analyzing=NO;
|
||||
return PGFail(error,@"当前目标的跟踪数据超过 64 MB。请将长素材分段分析;原先保存的数据仍保留。");
|
||||
}
|
||||
if(data) {
|
||||
if(![api setStringParameterValue:[data base64EncodedStringWithOptions:0] toParameter:PGTrackData1+_analysisSettings.targetIndex]) return PGFail(error,@"无法将分析数据保存到工程。");
|
||||
[api setBoolValue:YES toParameter:PGTrackEnabled atTime:_analysisTime];
|
||||
}
|
||||
[self setStatus:[NSString stringWithFormat:@"目标 %u:已分析 %lu 帧",_analysisSettings.targetIndex+1,(unsigned long)_analyzedFrames]];
|
||||
_analyzing=NO;return YES;
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>大鱼人像美化</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>17</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string></string>
|
||||
<key>PlugInKit</key>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<dict>
|
||||
<key>com.apple.protocol</key>
|
||||
<string>FxPlug</string>
|
||||
<key>com.apple.version</key>
|
||||
<string>1.1</string>
|
||||
</dict>
|
||||
<key>PrincipalClass</key>
|
||||
<string>FxPrincipal</string>
|
||||
<key>Protocol</key>
|
||||
<string>PROXPCProtocol</string>
|
||||
<key>Subsystems</key>
|
||||
<array>
|
||||
<string>NSViewService_PKSubsystem</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>ProPlugDictionaryVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>ProPlugDynamicRegistration</key>
|
||||
<false/>
|
||||
<key>ProPlugPlugInGroupList</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>groupName</key>
|
||||
<string>大鱼</string>
|
||||
<key>uuid</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>ProPlugPlugInList</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>FCPBeautyGirlPlugIn</string>
|
||||
<key>displayName</key>
|
||||
<string>大鱼人像美化</string>
|
||||
<key>group</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
<key>infoString</key>
|
||||
<string>人像磨皮、肤色、脸型和人脸跟踪</string>
|
||||
<key>protocolNames</key>
|
||||
<array>
|
||||
<string>FxFilter</string>
|
||||
<string>FxAnalyzer</string>
|
||||
</array>
|
||||
<key>uuid</key>
|
||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||
<key>version</key>
|
||||
<string>1.3.2</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>PGFaceSelectOSC</string>
|
||||
<key>displayName</key>
|
||||
<string>人脸选择</string>
|
||||
<key>group</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
<key>uuid</key>
|
||||
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
||||
<key>version</key>
|
||||
<string>1.3.2</string>
|
||||
<key>protocolNames</key>
|
||||
<array>
|
||||
<string>FxOnScreenControl</string>
|
||||
</array>
|
||||
<key>supportedPlugins</key>
|
||||
<array>
|
||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>XPCService</key>
|
||||
<dict>
|
||||
<key>ServiceType</key>
|
||||
<string>Application</string>
|
||||
<key>JoinExistingSession</key>
|
||||
<true/>
|
||||
<key>RunLoopType</key>
|
||||
<string>_NSApplicationMain</string>
|
||||
<key>_AdditionalSubServices</key>
|
||||
<dict>
|
||||
<key>viewbridge</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,27 @@
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Stateless, lazy Core Image graph. All masks use red=coverage and image coordinates.
|
||||
/// Caller owns face detection, geometric warping, tracking, and the CIContext/Metal device.
|
||||
/// showMask enables debugChannel 0=skin, 1=teeth, 2=eyes. invertMask affects each preview.
|
||||
/// If the caller blends geometry plus beauty against the original, pass settings.mix=1.
|
||||
@interface PGBeautyRenderer : NSObject
|
||||
- (nullable CIImage *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
skinMask:(nullable CIImage *)skinMask
|
||||
teethMask:(nullable CIImage *)teethMask
|
||||
eyeMask:(nullable CIImage *)eyeMask
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
/// Face-based callers supply uncut coverage for smoothly blended color changes.
|
||||
- (nullable CIImage *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
skinMask:(nullable CIImage *)skinMask
|
||||
colorMask:(nullable CIImage *)colorMask
|
||||
teethMask:(nullable CIImage *)teethMask
|
||||
eyeMask:(nullable CIImage *)eyeMask
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,310 @@
|
||||
#import "PGBeautyRenderer.h"
|
||||
#import <math.h>
|
||||
|
||||
// Original implementation reconstructed from observed processing stages, not vendor
|
||||
// resources. The learned segmentation model and LUTs are deliberately not embedded.
|
||||
static float PGClamp(float v, float lo, float hi) { return isfinite(v) ? fmaxf(lo, fminf(hi, v)) : lo; }
|
||||
static CIImage *PGBlur(CIImage *image, CGFloat radius, CGRect extent) {
|
||||
return [[[image imageByClampingToExtent] imageByApplyingFilter:@"CIGaussianBlur"
|
||||
withInputParameters:@{kCIInputRadiusKey:@(radius)}] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIColorKernel *PGKernel(NSString *body) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return [CIColorKernel kernelWithString:body];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
static CIImage *PGApply(CIColorKernel *kernel, CGRect extent, NSArray *arguments) {
|
||||
return [kernel applyWithExtent:extent arguments:arguments];
|
||||
}
|
||||
static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
|
||||
static NSDictionary *kernels;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
NSMutableDictionary *k = [NSMutableDictionary dictionary];
|
||||
// The fallback mask observes the shipped G/B overlay > Gaussian high-pass >
|
||||
// repeated hard-light stages. Semantic skin support is independently estimated.
|
||||
k[@"overlay"] = PGKernel(@
|
||||
"kernel vec4 pgGB(__sample p) { vec4 q=unpremultiply(p);"
|
||||
"float g=clamp(q.g*0.5,0.0,1.0); float b=clamp(q.b*0.5,0.0,1.0);"
|
||||
"float v=2.0*g*b; return vec4(v,v,v,1.0); }");
|
||||
k[@"candidate"] = PGKernel(@
|
||||
"kernel vec4 pgCandidate(__sample p,__sample g,__sample b) {"
|
||||
"vec3 c=clamp(unpremultiply(p).rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
|
||||
"float chroma=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
|
||||
"float h=clamp(g.r-b.r+0.5,0.0,1.0);"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"float band=clamp((h-0.29411765)*2.86516854,0.0,1.0);"
|
||||
"float edges=1.0-smoothstep(0.06,0.26,abs(band-0.59));"
|
||||
"float value=clamp(chroma*smoothstep(0.025,0.15,y)*(0.8+0.2*edges),0.0,1.0);"
|
||||
"return vec4(value,value,value,1.0); }");
|
||||
k[@"mask"] = PGKernel(@
|
||||
"kernel vec4 pgMask(__sample raw,__sample feather,__sample p,__sample b,__sample eyes,__sample teeth,"
|
||||
"float threshold,float softness,float edge,float protection,float invert) {"
|
||||
"float m=clamp(mix(raw.r,feather.r,softness),0.0,1.0);"
|
||||
"float width=0.025+0.24*softness;"
|
||||
"m=smoothstep(max(0.0,threshold-width),min(1.0,threshold+width),m);"
|
||||
"m=mix(m,1.0-m,invert);"
|
||||
"vec3 c=unpremultiply(p).rgb; vec3 l=unpremultiply(b).rgb;"
|
||||
"float structure=length(c-l);"
|
||||
"m*=1.0-edge*smoothstep(0.025,0.19,structure);"
|
||||
"float features=clamp(max(eyes.r,teeth.r),0.0,1.0);"
|
||||
"m*=1.0-protection*features;"
|
||||
"return vec4(m,m,m,1.0); }");
|
||||
// Independent frequency separation: texture and structure bands are compressed
|
||||
// in luminance, with feature/edge attenuation, keeping chroma and alpha stable.
|
||||
k[@"detail"] = PGKernel(@
|
||||
"kernel vec4 pgDetail(__sample p,__sample fine,__sample broad,float strength,float protect) {"
|
||||
"vec4 src=unpremultiply(p); vec3 c=src.rgb;"
|
||||
"vec3 t=unpremultiply(fine).rgb; vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114);"
|
||||
"float y=dot(c,w); float yf=dot(t,w); float yb=dot(b,w);"
|
||||
"float f=y-yf; float mid=yf-yb; float structure=length(c-b);"
|
||||
"float middle=smoothstep(0.04,0.22,y)*(1.0-smoothstep(0.82,0.98,y));"
|
||||
"float texture=smoothstep(0.001,0.010,abs(f));"
|
||||
"float edge=1.0-smoothstep(0.08,0.26,structure);"
|
||||
"float dark=(1.0-smoothstep(0.16,0.42,y))*smoothstep(0.0015,0.018,abs(f)+0.35*abs(y-yb));"
|
||||
"float light=smoothstep(0.70,0.96,y)*smoothstep(0.0025,0.024,abs(f));"
|
||||
"float ridge=smoothstep(0.10,0.30,structure)*smoothstep(0.002,0.020,abs(f)+0.85*abs(y-yb));"
|
||||
"float feature=max(dark*0.96,max(light*0.52,ridge*0.84));"
|
||||
"float gain=clamp(strength*middle*mix(0.56,0.88,texture),0.0,1.05);"
|
||||
"gain*=1.0-protect*feature*mix(0.42,0.82,texture);"
|
||||
"float delta=-gain*(f*mix(0.18,0.76,edge)+mid*mix(0.08,0.30,edge));"
|
||||
"float target=clamp(y+delta,0.0,1.0); float ratio=clamp(target/max(y,0.001),0.82,1.18);"
|
||||
"vec3 outColor=c*mix(1.0,ratio,0.88);"
|
||||
"outColor+=vec3((target-dot(outColor,w))*0.08);"
|
||||
"return premultiply(vec4(clamp(outColor,0.0,1.0),src.a)); }");
|
||||
k[@"maskedMix"] = PGKernel(@
|
||||
"kernel vec4 pgMaskedMix(__sample source,__sample effect,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec4 e=unpremultiply(effect);"
|
||||
"return premultiply(vec4(mix(s.rgb,e.rgb,clamp(mask.r*amount,0.0,1.0)),s.a)); }");
|
||||
// Keep smoothing support separate from the exposure/color mask. The
|
||||
// two new controls independently release feature cutouts and background.
|
||||
k[@"smoothingSupport"] = PGKernel(@
|
||||
"kernel vec4 pgSmoothingSupport(__sample protectedMask,__sample baseMask,float featureProtection,float skinProtection) {"
|
||||
"float base=clamp(baseMask.r,0.0,1.0);float protectedValue=clamp(protectedMask.r,0.0,1.0);"
|
||||
"float value=mix(base,protectedValue,featureProtection);"
|
||||
"if(skinProtection<1.0){float extended=1.0-skinProtection*(1.0-base);"
|
||||
"float retention=base>0.00001?clamp(protectedValue/base,0.0,1.0):1.0;"
|
||||
"value=extended*mix(1.0,retention,featureProtection);}"
|
||||
"return vec4(value,value,value,1.0); }");
|
||||
k[@"microDetail"] = PGKernel(@
|
||||
"kernel vec4 pgMicroDetail(__sample processed,__sample original,__sample low,__sample coverage,float amount) {"
|
||||
"vec4 outPixel=unpremultiply(processed);vec3 src=unpremultiply(original).rgb;"
|
||||
"vec3 high=src-unpremultiply(low).rgb;vec3 weights=vec3(0.299,0.587,0.114);"
|
||||
"float y=dot(src,weights);float detailY=dot(high,weights);float size=length(high);"
|
||||
"float lightGate=smoothstep(0.07,0.22,y)*(1.0-smoothstep(0.86,0.99,y));"
|
||||
"float textureGate=smoothstep(0.0015,0.010,size)*(1.0-smoothstep(0.16,0.36,size));"
|
||||
"float gain=amount*clamp(coverage.r,0.0,1.0)*lightGate*textureGate*1.18;"
|
||||
"vec3 detail=vec3(detailY*0.92)+(high-vec3(detailY))*0.28;"
|
||||
"return premultiply(vec4(clamp(outPixel.rgb+detail*gain,0.0,1.0),outPixel.a)); }");
|
||||
k[@"colorCoverage"] = PGKernel(@
|
||||
"kernel vec4 pgColorCoverage(__sample coverage,__sample image,float invert) {"
|
||||
"vec3 c=unpremultiply(image).rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
|
||||
"float likelihood=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
|
||||
"float v=clamp(coverage.r,0.0,1.0)*smoothstep(0.05,0.60,likelihood);"
|
||||
"v=mix(v,1.0-v,invert); return vec4(v,v,v,1.0); }");
|
||||
k[@"even"] = PGKernel(@
|
||||
"kernel vec4 pgEven(__sample source,__sample broad,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114); float y=dot(s.rgb,w); float yb=dot(b,w);"
|
||||
"vec3 baseChroma=s.rgb-vec3(y); vec3 smoothChroma=b-vec3(yb);"
|
||||
"float guard=1.0-smoothstep(0.12,0.35,length(s.rgb-b));"
|
||||
"vec3 c=vec3(y)+mix(baseChroma,smoothChroma,amount*0.78*guard);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
// These six looks are newly authored color transforms, not the shipped LUTs.
|
||||
k[@"preset"] = PGKernel(@
|
||||
"kernel vec4 pgPreset(__sample source,float preset) {"
|
||||
"vec4 s=unpremultiply(source); vec3 c=s.rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"if(preset<1.5){c=mix(vec3(y),c,0.91);c+=vec3(0.038,0.027,0.012)*(1.0-y);}"
|
||||
"else if(preset<2.5){c=mix(vec3(y),c,0.89);c+=vec3(0.003,0.020,0.038)*(1.0-y);}"
|
||||
"else if(preset<3.5){c=mix(vec3(y),c,1.04);c+=vec3(0.037,0.011,-0.018)*(1.0-y);}"
|
||||
"else if(preset<4.5){c=mix(vec3(y),c,0.94);c+=vec3(0.032,0.001,0.018)*(1.0-y);}"
|
||||
"else if(preset<5.5){c=pow(max(c,vec3(0.0)),vec3(1.08));c+=vec3(0.024,0.003,-0.018)*(1.0-y);}"
|
||||
"else{c=pow(max(c,vec3(0.0)),vec3(1.20));c=mix(vec3(dot(c,vec3(0.299,0.587,0.114))),c,1.06);"
|
||||
"c+=vec3(0.030,0.007,-0.022)*(1.0-y);}"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"teeth"] = PGKernel(@
|
||||
"kernel vec4 pgTeeth(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);vec3 c=clamp(s.rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));float hi=max(c.r,max(c.g,c.b));float lo=min(c.r,min(c.g,c.b));"
|
||||
"float sat=(hi-lo)/max(hi,0.0001);"
|
||||
"float gate=smoothstep(0.26,0.56,max(y,hi*0.86))*(1.0-smoothstep(0.52,0.86,sat));"
|
||||
"float weight=clamp(pow(max(0.0,smoothstep(0.08,0.92,mask.r)*gate*amount),0.90)*(1.02+0.18*amount),0.0,0.88);"
|
||||
"float yellow=max(0.0,(c.r+c.g)*0.5-c.b);"
|
||||
"vec3 neutral=mix(c,vec3(y),0.16+0.22*amount);"
|
||||
"neutral+=yellow*vec3(-0.070*amount,-0.028*amount,0.18+0.28*amount);"
|
||||
"neutral+=(vec3(1.0)-neutral)*(0.045+0.155*amount);"
|
||||
"return premultiply(vec4(mix(c,clamp(neutral,0.0,1.0),weight),s.a)); }");
|
||||
k[@"eyes"] = PGKernel(@
|
||||
"kernel vec4 pgEyes(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);float y=dot(s.rgb,vec3(0.299,0.587,0.114));"
|
||||
"float weight=clamp(mask.r,0.0,1.0)*amount*smoothstep(0.08,0.32,y);"
|
||||
"vec3 c=s.rgb+(vec3(1.0)-s.rgb)*(0.18*weight);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"debug"] = PGKernel(@
|
||||
"kernel vec4 pgDebug(__sample source,__sample mask,float invert) {"
|
||||
"vec4 s=unpremultiply(source);float m=clamp(mask.r,0.0,1.0);"
|
||||
"m=mix(m,1.0-m,invert);return premultiply(vec4(vec3(m),s.a)); }");
|
||||
kernels = [k copy];
|
||||
});
|
||||
return kernels;
|
||||
}
|
||||
|
||||
@implementation PGBeautyRenderer
|
||||
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
|
||||
skinMask:(CIImage *)skinMask teethMask:(CIImage *)teethMask
|
||||
eyeMask:(CIImage *)eyeMask error:(NSError **)error {
|
||||
return [self processImage:image settings:s skinMask:skinMask colorMask:nil teethMask:teethMask eyeMask:eyeMask error:error];
|
||||
}
|
||||
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
|
||||
skinMask:(CIImage *)skinMask colorMask:(CIImage *)colorMask
|
||||
teethMask:(CIImage *)teethMask eyeMask:(CIImage *)eyeMask error:(NSError **)error {
|
||||
if (error) *error = nil;
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsEmpty(extent) || CGRectIsInfinite(extent) ||
|
||||
!isfinite(extent.origin.x) || !isfinite(extent.origin.y) ||
|
||||
!isfinite(extent.size.width) || !isfinite(extent.size.height)) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The source image needs a finite nonempty extent."}];
|
||||
return nil;
|
||||
}
|
||||
float mix = PGClamp(s.mix, 0, 1);
|
||||
if (mix == 0 && !s.showMask) return image;
|
||||
NSDictionary *k = PGKernels();
|
||||
if (k.count != 13) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}];
|
||||
return nil;
|
||||
}
|
||||
@try {
|
||||
float amount = PGClamp(s.amount,0,1);
|
||||
float local = PGClamp(s.midtoneDetail,0,2)*0.5f;
|
||||
float global = PGClamp(s.highPassDetail,0,1);
|
||||
float brightness = PGClamp(s.skinBrightness,0,1);
|
||||
float hue = PGClamp(s.skinHue,-30,30);
|
||||
float saturation = PGClamp(s.skinSaturation,-100,100);
|
||||
float even = PGClamp(s.skinEvenTone,0,1);
|
||||
float sharpness = PGClamp(s.sharpness,0,2);
|
||||
float teeth = PGClamp(s.teethWhitening,0,1);
|
||||
float eyes = PGClamp(s.eyeBrightening,0,1);
|
||||
float protection = s.featureProtection ? PGClamp(s.featureProtectionAmount,0,1) : 0;
|
||||
float skinProtection = PGClamp(s.nonSkinProtection,0,1);
|
||||
float micro = PGClamp(s.microDetail,0,1)*amount;
|
||||
BOOL needsSkin = local*amount>0 || micro>0 || brightness>0 || hue!=0 || saturation!=0 || even>0 ||
|
||||
(s.skinTonePreset>0 && s.skinTonePreset<=6) || (s.showMask && s.debugChannel==0);
|
||||
CIImage *black = [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
CIImage *teethInput = teethMask ? [teethMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *eyeInput = eyeMask ? [eyeMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *raw = skinMask ? [skinMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *mask = black;
|
||||
CGFloat radius = PGClamp(s.radius,1,40);
|
||||
if (needsSkin) {
|
||||
if (!skinMask) {
|
||||
CIImage *gb = PGApply(k[@"overlay"],extent,@[image]);
|
||||
raw = PGApply(k[@"candidate"],extent,@[image,gb,PGBlur(gb,radius,extent)]);
|
||||
}
|
||||
float soft = PGClamp(s.maskSoftness,0,1);
|
||||
float threshold = PGClamp(s.maskThreshold,0,255)/255.f;
|
||||
mask = PGApply(k[@"mask"],extent,@[raw,PGBlur(raw,0.35+soft*radius*0.35,extent),
|
||||
image,PGBlur(image,1.5,extent),eyeInput,teethInput,
|
||||
@(threshold),@(soft),@(PGClamp(s.maskEdgeProtect,0,1)),
|
||||
@(s.featureProtection ? 1 : 0),@(s.invertMask ? 1 : 0)]);
|
||||
}
|
||||
if (s.showMask) {
|
||||
// Popup values select coverage source; changing the popup alone must
|
||||
// never enable a preview. Feature masks retain their supplied coverage.
|
||||
CIImage *preview = s.debugChannel==1 ? teethInput : (s.debugChannel==2 ? eyeInput : mask);
|
||||
float invert = s.debugChannel==1 || s.debugChannel==2 ? (s.invertMask ? 1 : 0) : 0;
|
||||
return PGApply(k[@"debug"],extent,@[image,preview,@(invert)]);
|
||||
}
|
||||
CIImage *smoothingMask=mask;
|
||||
if((local*amount>0 || micro>0) && (protection<1 || skinProtection<1)) {
|
||||
CIImage *baseRaw=colorMask ? [colorMask imageByCroppingToRect:extent] : raw;
|
||||
float soft=PGClamp(s.maskSoftness,0,1);
|
||||
CIImage *baseMask=PGApply(k[@"mask"],extent,@[baseRaw,PGBlur(baseRaw,0.35+soft*radius*0.35,extent),
|
||||
image,PGBlur(image,1.5,extent),eyeInput,teethInput,
|
||||
@(PGClamp(s.maskThreshold,0,255)/255.f),@(soft),@(PGClamp(s.maskEdgeProtect,0,1)),@0,@(s.invertMask?1:0)]);
|
||||
smoothingMask=PGApply(k[@"smoothingSupport"],extent,@[mask,baseMask,@(protection),@(skinProtection)]);
|
||||
}
|
||||
// Detail protection may contain small eye, brow, nose and lip cutouts.
|
||||
// Exposure and color must not reuse those holes or sharpen a face ellipse
|
||||
// with the detail threshold; that creates visible rings and islands.
|
||||
CIImage *toneMask=mask;
|
||||
BOOL changesColor=brightness>0 || hue!=0 || saturation!=0 || even>0 ||
|
||||
(s.skinTonePreset>0 && s.skinTonePreset<=6);
|
||||
if(!colorMask && changesColor && s.featureProtection) {
|
||||
// Standalone callers may not supply the separate uncut coverage.
|
||||
// At least keep our eye/teeth cutouts out of their color adjustment.
|
||||
float soft=PGClamp(s.maskSoftness,0,1);
|
||||
toneMask=PGApply(k[@"mask"],extent,@[raw,PGBlur(raw,0.35+soft*radius*0.35,extent),
|
||||
image,PGBlur(image,1.5,extent),eyeInput,teethInput,
|
||||
@(PGClamp(s.maskThreshold,0,255)/255.f),@(soft),@(PGClamp(s.maskEdgeProtect,0,1)),@0,@(s.invertMask?1:0)]);
|
||||
}
|
||||
if(colorMask && changesColor) {
|
||||
CGFloat feather=MAX(3,MIN(extent.size.width,extent.size.height)*0.03);
|
||||
CIImage *support=[colorMask imageByCroppingToRect:extent];
|
||||
toneMask=PGApply(k[@"colorCoverage"],extent,@[PGBlur(support,feather,extent),
|
||||
PGBlur(image,MAX(1,feather*0.15),extent),@(s.invertMask?1:0)]);
|
||||
}
|
||||
CIImage *result = image;
|
||||
if (local*amount>0) {
|
||||
// The shipped helper derives both blur scales from the detail strength.
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(local,0.72)+0.95*local);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*local)*fine+1.1+1.3*local);
|
||||
float compression = 0.12+2.4*powf(local,2.15)+1.25*local;
|
||||
CIImage *detail = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@(protection)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,detail,smoothingMask,@(amount)]);
|
||||
}
|
||||
if(micro>0) {
|
||||
CGFloat fine=fmin(7.4,2.275+3.15*pow(local,0.72)+0.95*local);
|
||||
result=PGApply(k[@"microDetail"],extent,@[result,image,PGBlur(image,fine,extent),smoothingMask,@(micro)]);
|
||||
}
|
||||
CIImage *color = result;
|
||||
if (brightness>0) {
|
||||
float shoulder = PGClamp((brightness-0.56f)/0.44f,0,1);
|
||||
float gain = 0.88f*powf(brightness,1.24f)+0.12f*shoulder*shoulder*(3-2*shoulder);
|
||||
color = [color imageByApplyingFilter:@"CIExposureAdjust" withInputParameters:@{kCIInputEVKey:@(gain*0.95f)}];
|
||||
}
|
||||
if (hue!=0) color = [color imageByApplyingFilter:@"CIHueAdjust" withInputParameters:@{kCIInputAngleKey:@(hue*M_PI/180.0)}];
|
||||
if (saturation!=0) color = [color imageByApplyingFilter:@"CIColorControls" withInputParameters:@{kCIInputSaturationKey:@(1+saturation/100.f)}];
|
||||
if (even>0) color = PGApply(k[@"even"],extent,@[color,PGBlur(color,radius*1.8,extent),@(even)]);
|
||||
if (color!=result) result = PGApply(k[@"maskedMix"],extent,@[result,color,toneMask,@1]);
|
||||
if (sharpness*amount>0) result = [result imageByApplyingFilter:@"CISharpenLuminance"
|
||||
withInputParameters:@{kCIInputSharpnessKey:@(sharpness*amount)}];
|
||||
if (s.skinTonePreset>0 && s.skinTonePreset<=6) {
|
||||
CIImage *preset = PGApply(k[@"preset"],extent,@[result,@(s.skinTonePreset)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,preset,toneMask,@1]);
|
||||
}
|
||||
if (global>0) {
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(global,0.72)+0.95*global);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*global)*fine+1.1+1.3*global);
|
||||
float compression = 0.12+2.4*powf(global,2.15)+1.25*global;
|
||||
result = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@0]);
|
||||
}
|
||||
if (teeth>0 && teethMask) result = PGApply(k[@"teeth"],extent,@[result,teethInput,@(teeth)]);
|
||||
if (eyes>0 && eyeMask) result = PGApply(k[@"eyes"],extent,@[result,eyeInput,@(eyes)]);
|
||||
if (mix<1) {
|
||||
CIImage *white = [[CIImage imageWithColor:[CIColor colorWithRed:1 green:1 blue:1 alpha:1]] imageByCroppingToRect:extent];
|
||||
result = PGApply(k[@"maskedMix"],extent,@[image,result,white,@(mix)]);
|
||||
}
|
||||
if (!result) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:3
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"Core Image could not construct the beauty render graph."}];
|
||||
return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
} @catch (NSException *exception) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:4
|
||||
userInfo:@{NSLocalizedDescriptionKey:exception.reason ?: @"Invalid Core Image render state."}];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
// Persisted FxPlug render state. Append fields and bump version for migrations.
|
||||
typedef struct {
|
||||
uint32_t version;
|
||||
float amount, skinBrightness, skinHue, skinSaturation, skinEvenTone;
|
||||
float midtoneDetail, highPassDetail, teethWhitening, eyeBrightening;
|
||||
float radius, sharpness, mix, maskThreshold, maskSoftness, maskEdgeProtect;
|
||||
float faceThin, faceBigEye, faceShort, faceNarrow, faceChin, faceV;
|
||||
float faceJaw, faceCheekbone, faceSmallHead, faceSmallHeadRange;
|
||||
float faceJumpGuardRange, selectionX, selectionY;
|
||||
uint32_t featureProtection, skinTonePreset, showMask, invertMask, debugChannel;
|
||||
uint32_t showFaceBoxes, showTrackBox, singleFace, fastMultiFace, trackingEnabled, targetIndex;
|
||||
// v2 keeps the entire 160-byte v1 prefix, including legacy bool/strength IDs.
|
||||
float featureProtectionAmount, nonSkinProtection, microDetail;
|
||||
} PGBeautySettings;
|
||||
|
||||
enum { PGBeautySettingsV1Size = 160, PGBeautySettingsVersion = 2 };
|
||||
_Static_assert(offsetof(PGBeautySettings,featureProtectionAmount)==PGBeautySettingsV1Size,
|
||||
"The v1 render-state prefix must remain stable");
|
||||
|
||||
static inline PGBeautySettings PGBeautyDefaults(void) {
|
||||
PGBeautySettings s = {0};
|
||||
s.version = PGBeautySettingsVersion;
|
||||
s.amount = 1.0f;
|
||||
s.radius = 8.0f;
|
||||
s.mix = 1.0f;
|
||||
s.maskThreshold = 148.0f;
|
||||
s.maskSoftness = 0.6f;
|
||||
s.maskEdgeProtect = 0.4f;
|
||||
s.featureProtection = 1;
|
||||
s.featureProtectionAmount = 1;
|
||||
s.nonSkinProtection = 1;
|
||||
s.faceJumpGuardRange = 1.8f;
|
||||
s.selectionX = s.selectionY = 0.5f;
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PGFaceFrame : NSObject
|
||||
/// Vision observations in normalized, bottom-left image coordinates. Restored
|
||||
/// observations retain their bounding box; render geometry is stored separately.
|
||||
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
|
||||
/// Stable, normalized control geometry used for image and mask deformation.
|
||||
/// Default realtime processing deforms one primary face while faces/masks may
|
||||
/// cover all current detections. Explicit fast-multi mode deforms every face.
|
||||
@property (nonatomic, copy, readonly) NSArray<NSDictionary *> *deformationGeometry;
|
||||
@property (nonatomic, strong, readonly) CIImage *skinMask;
|
||||
/// Continuous face support before eye/nose/lip detail-protection cutouts.
|
||||
@property (nonatomic, strong, readonly) CIImage *colorMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *image;
|
||||
/// Transparent face/target outlines, for compositing after beauty processing.
|
||||
@property (nonatomic, strong, readonly) CIImage *overlayImage;
|
||||
@end
|
||||
|
||||
@interface PGFaceProcessor : NSObject
|
||||
- (nullable PGFaceFrame *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (BOOL)analyzeImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (void)resetTracking;
|
||||
/// Targets are zero-based (0, 1, 2). Data is a versioned property-list archive,
|
||||
/// independent of Vision implementation classes. Only analysis writes these
|
||||
/// persisted frames; realtime geometry has a separate bounded, in-memory cache.
|
||||
- (nullable NSData *)trackingDataForTarget:(NSUInteger)target;
|
||||
- (void)setTrackingData:(nullable NSData *)data forTarget:(NSUInteger)target;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,767 @@
|
||||
// The source kernels remain available on the supported macOS 12+ range. This
|
||||
// keeps the processor self contained instead of requiring a separate metallib.
|
||||
#define CI_SILENCE_GL_DEPRECATION 1
|
||||
#import "PGFaceProcessor.h"
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
|
||||
static NSString * const PGFaceErrorDomain = @"com.dayu.fcpbeautygirl.face";
|
||||
// Contour origin / internal anchor, in the reference's recovered pin order.
|
||||
// The unused middle entry of each reference triplet is intentionally omitted.
|
||||
static const uint8_t PGThinPins[17][2] = {
|
||||
{75,6},{59,13},{74,47},{60,47},{73,47},{61,47},
|
||||
{72,48},{62,48},{71,48},{63,48},{70,49},{64,49},
|
||||
{69,49},{65,49},{68,52},{66,52},{67,47}
|
||||
};
|
||||
static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}};
|
||||
// Lower contour to lip anchors, in the 2.4.0 V-face shader's update order.
|
||||
static const uint8_t PGVFacePins[4][2] = {{69,39},{65,35},{70,26},{64,34}};
|
||||
// Origin and radius neighbour; all six directions point toward landmark 29.
|
||||
static const uint8_t PGJawPins[6][2] = {{72,71},{62,63},{71,70},{63,64},{70,71},{64,63}};
|
||||
|
||||
@interface PGFaceFrame ()
|
||||
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
||||
@property (nonatomic, copy, readwrite) NSArray<NSDictionary *> *deformationGeometry;
|
||||
@property (nonatomic, strong, readwrite) CIImage *skinMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *colorMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *image;
|
||||
@property (nonatomic, strong, readwrite) CIImage *overlayImage;
|
||||
@end
|
||||
@implementation PGFaceFrame
|
||||
@end
|
||||
|
||||
// All geometry in stored records is normalized to the full source image, with
|
||||
// Y increasing upward. Pixel size, render scale and tile order are not persisted.
|
||||
static NSString *PGTimeKey(CMTime time) {
|
||||
if (!CMTIME_IS_NUMERIC(time) || time.timescale <= 0) return nil;
|
||||
int64_t a = time.value, b = time.timescale;
|
||||
// Unsigned magnitude avoids overflow for the minimum signed integer.
|
||||
uint64_t u = a < 0 ? (uint64_t)(-(a + 1)) + 1 : (uint64_t)a;
|
||||
uint64_t v = (uint64_t)b;
|
||||
while (v) { uint64_t r = u % v; u = v; v = r; }
|
||||
int64_t divisor = (int64_t)(u ?: 1);
|
||||
return [NSString stringWithFormat:@"%lld/%lld/%lld", (long long)(a / divisor),
|
||||
(long long)(b / divisor), (long long)time.epoch];
|
||||
}
|
||||
|
||||
static float PGClamp(float value, float low, float high) {
|
||||
return isfinite(value) ? fminf(high, fmaxf(low, value)) : 0;
|
||||
}
|
||||
static NSArray *PGRectArray(CGRect rect) {
|
||||
return @[@(rect.origin.x), @(rect.origin.y), @(rect.size.width), @(rect.size.height)];
|
||||
}
|
||||
static CGRect PGArrayRect(NSArray *array) {
|
||||
if (![array isKindOfClass:NSArray.class] || array.count != 4) return CGRectZero;
|
||||
for (id value in array) if (![value isKindOfClass:NSNumber.class] || !isfinite([value doubleValue])) return CGRectZero;
|
||||
return CGRectMake([array[0] doubleValue], [array[1] doubleValue], [array[2] doubleValue], [array[3] doubleValue]);
|
||||
}
|
||||
static BOOL PGValidRect(CGRect rect) {
|
||||
return rect.size.width > 0.001 && rect.size.height > 0.001 &&
|
||||
rect.origin.x >= -0.1 && rect.origin.y >= -0.1 && CGRectGetMaxX(rect) <= 1.1 && CGRectGetMaxY(rect) <= 1.1;
|
||||
}
|
||||
static CGPoint PGCenter(CGRect rect) { return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); }
|
||||
static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||
if (!region || region.pointCount == 0) return @[];
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:region.pointCount];
|
||||
const CGPoint *p = region.normalizedPoints;
|
||||
if (!p) return @[];
|
||||
for (NSUInteger i = 0; i < MIN(region.pointCount, 256); i++) {
|
||||
if (isfinite(p[i].x) && isfinite(p[i].y)) {
|
||||
[points addObject:@[@(box.origin.x + p[i].x * box.size.width),
|
||||
@(box.origin.y + p[i].y * box.size.height)]];
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
// This optional field has a fixed topology. Never remove an invalid point and
|
||||
// shift the remaining indices: the local pins refer to exact Vision 76 indices.
|
||||
static NSArray *PGSanitizedPoints76(id value) {
|
||||
if (![value isKindOfClass:NSArray.class] || [value count] != 76) return nil;
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (id point in value) {
|
||||
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
|
||||
for (id coordinate in point) {
|
||||
if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
|
||||
}
|
||||
[points addObject:[point copy]];
|
||||
}
|
||||
return [points copy];
|
||||
}
|
||||
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||
if (region.pointCount != 76 || !region.normalizedPoints) return nil;
|
||||
const CGPoint *raw = region.normalizedPoints;
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (NSUInteger i=0; i<76; i++) {
|
||||
if (!isfinite(raw[i].x) || !isfinite(raw[i].y)) return nil;
|
||||
[points addObject:@[@(box.origin.x+raw[i].x*box.size.width),
|
||||
@(box.origin.y+raw[i].y*box.size.height)]];
|
||||
}
|
||||
return PGSanitizedPoints76(points);
|
||||
}
|
||||
static NSDictionary *PGGeometry(VNFaceObservation *observation) {
|
||||
CGRect box = observation.boundingBox;
|
||||
VNFaceLandmarks2D *lm = observation.landmarks;
|
||||
NSMutableDictionary *geometry = [@{@"box": PGRectArray(box),
|
||||
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
|
||||
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
|
||||
@"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, box),
|
||||
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
|
||||
NSArray *points = PGFullPoints76(lm.allPoints, box);
|
||||
if (points) geometry[@"points76"] = points;
|
||||
return [geometry copy];
|
||||
}
|
||||
static NSDictionary *PGSanitizedGeometry(id value) {
|
||||
if (![value isKindOfClass:NSDictionary.class]) return nil;
|
||||
CGRect box = PGArrayRect(value[@"box"]);
|
||||
if (!PGValidRect(box)) return nil;
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithObject:PGRectArray(box) forKey:@"box"];
|
||||
for (NSString *name in @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"nose", @"lips", @"mouth", @"contour"]) {
|
||||
id raw = value[name];
|
||||
if (![raw isKindOfClass:NSArray.class] || [raw count] > 256) return nil;
|
||||
NSMutableArray *points = [NSMutableArray array];
|
||||
for (id point in raw) {
|
||||
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
|
||||
for (id coordinate in point) if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
|
||||
[points addObject:[point copy]];
|
||||
}
|
||||
result[name] = points;
|
||||
}
|
||||
// Old archives and partially detected faces remain usable by the other
|
||||
// controls. Missing or malformed full topology disables the recovered local
|
||||
// thin/chin/jaw/short fields without discarding otherwise usable old records.
|
||||
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
|
||||
if (points76) result[@"points76"] = points76;
|
||||
return [result copy];
|
||||
}
|
||||
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
||||
NSArray *points = geometry[name];
|
||||
if (points.count == 0) return fallback;
|
||||
CGFloat minX = 2, minY = 2, maxX = -1, maxY = -1;
|
||||
for (NSArray *point in points) {
|
||||
CGFloat x = [point[0] doubleValue], y = [point[1] doubleValue];
|
||||
minX = MIN(minX, x); minY = MIN(minY, y); maxX = MAX(maxX, x); maxY = MAX(maxY, y);
|
||||
}
|
||||
return CGRectMake(minX, minY, MAX(0.001, maxX - minX), MAX(0.001, maxY - minY));
|
||||
}
|
||||
static CGRect PGInFace(CGRect face, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
||||
return CGRectMake(face.origin.x + x * face.size.width, face.origin.y + y * face.size.height,
|
||||
w * face.size.width, h * face.size.height);
|
||||
}
|
||||
static CGRect PGToPixels(CGRect normalized, CGRect extent) {
|
||||
return CGRectMake(extent.origin.x + normalized.origin.x * extent.size.width,
|
||||
extent.origin.y + normalized.origin.y * extent.size.height,
|
||||
normalized.size.width * extent.size.width, normalized.size.height * extent.size.height);
|
||||
}
|
||||
static CIVector *PGVector(CGPoint point) { return [CIVector vectorWithX:point.x Y:point.y]; }
|
||||
static CIImage *PGBlack(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGTransparent(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:0]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
||||
return [a imageByApplyingFilter:@"CIMaximumCompositing" withInputParameters:@{kCIInputBackgroundImageKey:b}];
|
||||
}
|
||||
|
||||
static CMTime PGRecordTime(NSDictionary *record) {
|
||||
return CMTimeMakeWithEpoch([record[@"value"] longLongValue], [record[@"scale"] intValue], [record[@"epoch"] longLongValue]);
|
||||
}
|
||||
static double PGSeedDistance(NSDictionary *record, PGBeautySettings settings) {
|
||||
return hypot([record[@"seedX"] doubleValue]-settings.selectionX, [record[@"seedY"] doubleValue]-settings.selectionY);
|
||||
}
|
||||
static NSArray<NSString *> *PGGeometryRegions(void) {
|
||||
return @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"];
|
||||
}
|
||||
static CGPoint PGRemapPoint(NSArray *point, CGRect old, CGRect current) {
|
||||
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
|
||||
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
|
||||
}
|
||||
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt, BOOL stabilizePoints76) {
|
||||
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
|
||||
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
||||
// Independent stabilization: translate/scale old landmarks into the current
|
||||
// detection box first, so normal head motion does not accumulate box lag.
|
||||
double weight=.9*fmin(1,fmax(0,dt/.03));
|
||||
CGPoint oldLE=PGCenter(PGRegionBounds(previous,@"leftEye",PGInFace(old,.16,.53,.25,.15)));
|
||||
CGPoint oldRE=PGCenter(PGRegionBounds(previous,@"rightEye",PGInFace(old,.59,.53,.25,.15)));
|
||||
CGPoint newLE=PGCenter(PGRegionBounds(current,@"leftEye",PGInFace(box,.16,.53,.25,.15)));
|
||||
CGPoint newRE=PGCenter(PGRegionBounds(current,@"rightEye",PGInFace(box,.59,.53,.25,.15)));
|
||||
double ox=oldRE.x-oldLE.x,oy=oldRE.y-oldLE.y,nx=newRE.x-newLE.x,ny=newRE.y-newLE.y;
|
||||
double oldLength=hypot(ox,oy),newLength=hypot(nx,ny);
|
||||
BOOL axisHold=oldLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||
newLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||
(ox*nx+oy*ny)/(oldLength*newLength)<-.18;
|
||||
NSMutableDictionary *stable=[NSMutableDictionary dictionaryWithObject:current[@"box"] forKey:@"box"];
|
||||
for(NSString *name in PGGeometryRegions()) {
|
||||
NSArray *a=previous[name],*b=current[name];
|
||||
if(!a.count || (!axisHold && a.count!=b.count)) {stable[name]=b;continue;}
|
||||
NSMutableArray *points=[NSMutableArray arrayWithCapacity:a.count];
|
||||
for(NSUInteger i=0;i<a.count;i++) {
|
||||
CGPoint p=PGRemapPoint(a[i],old,box);
|
||||
if(!axisHold) {p.x+=( [b[i][0] doubleValue]-p.x)*weight;p.y+=([b[i][1] doubleValue]-p.y)*weight;}
|
||||
[points addObject:@[@(p.x),@(p.y)]];
|
||||
}
|
||||
stable[name]=[points copy];
|
||||
}
|
||||
NSArray *current76 = PGSanitizedPoints76(current[@"points76"]);
|
||||
NSArray *previous76 = PGSanitizedPoints76(previous[@"points76"]);
|
||||
if (current76) {
|
||||
if (stabilizePoints76 && previous76) {
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (NSUInteger i=0; i<76; i++) {
|
||||
CGPoint p = PGRemapPoint(previous76[i], old, box);
|
||||
if (!axisHold) {
|
||||
p.x += ([current76[i][0] doubleValue]-p.x)*weight;
|
||||
p.y += ([current76[i][1] doubleValue]-p.y)*weight;
|
||||
}
|
||||
[points addObject:@[@(p.x),@(p.y)]];
|
||||
}
|
||||
stable[@"points76"] = [points copy];
|
||||
} else {
|
||||
// Reacquired detections and changes from/to incomplete topology do
|
||||
// not borrow a held face's old projected pose or invent missing points.
|
||||
stable[@"points76"] = current76;
|
||||
}
|
||||
}
|
||||
return [stable copy];
|
||||
}
|
||||
|
||||
// This live selector is deliberately independent of _targets. Its past-only
|
||||
// .35s reference window is a documented project policy, not a recovered exact
|
||||
// vendor formula. Continuous held outputs advance time, so this is not a hold
|
||||
// timeout. Locks protect state, not global render-order equivalence: a cold seek
|
||||
// cannot reconstruct frames the host has never supplied.
|
||||
@interface PGRealtimeFaceTracker : NSObject {
|
||||
NSLock *_mutex;
|
||||
NSMutableArray<NSDictionary *> *_entries;
|
||||
NSUInteger _estimatedBytes;
|
||||
}
|
||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||
- (void)reset;
|
||||
- (NSDictionary *)cacheStatistics;
|
||||
@end
|
||||
@implementation PGRealtimeFaceTracker
|
||||
- (instancetype)init {if((self=[super init])){_mutex=[NSLock new];_entries=[NSMutableArray new];}return self;}
|
||||
- (void)reset {[_mutex lock];[_entries removeAllObjects];_estimatedBytes=0;[_mutex unlock];}
|
||||
- (NSDictionary *)cacheStatistics {[_mutex lock];NSDictionary *stats=@{@"frames":@(_entries.count),@"estimatedBytes":@(_estimatedBytes)};[_mutex unlock];return stats;}
|
||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)input settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent {
|
||||
NSMutableArray *valid=[NSMutableArray new];
|
||||
for(id item in input) {NSDictionary *g=PGSanitizedGeometry(item);if(g && valid.count<16)[valid addObject:g];}
|
||||
NSArray *raw=[valid copy];
|
||||
NSUInteger mode=s.fastMultiFace?2:(s.singleFace?1:0);
|
||||
NSString *key=PGTimeKey(time);
|
||||
NSString *context=[NSString stringWithFormat:@"%lu/%u/%lld/%.17g/%.17g/%.17g/%.17g/%.9g",(unsigned long)mode,s.targetIndex,
|
||||
(long long)time.epoch,extent.origin.x,extent.origin.y,extent.size.width,extent.size.height,s.faceJumpGuardRange];
|
||||
[_mutex lock];
|
||||
NSDictionary *head=nil;
|
||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] &&
|
||||
(!head || CMTimeCompare(PGRecordTime(record),PGRecordTime(head))>=0)) head=record;
|
||||
NSString *scope=@"automatic";
|
||||
double seedX=PGClamp(s.selectionX,0,1),seedY=PGClamp(s.selectionY,0,1);
|
||||
if(mode==1) {
|
||||
// A real point change (> .01) starts a new identity scope. Older render
|
||||
// snapshots may use their own past scope, but never replace a newer head.
|
||||
NSDictionary *seedRecord=head;
|
||||
if(seedRecord && CMTimeCompare(time,PGRecordTime(seedRecord))<0) {
|
||||
seedRecord=nil;
|
||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] && PGSeedDistance(record,s)<=.01 &&
|
||||
CMTimeCompare(PGRecordTime(record),time)<=0 && (!seedRecord || CMTimeCompare(PGRecordTime(record),PGRecordTime(seedRecord))>=0))seedRecord=record;
|
||||
}
|
||||
if(seedRecord && PGSeedDistance(seedRecord,s)<=.01) {
|
||||
scope=seedRecord[@"scope"];seedX=[seedRecord[@"seedX"] doubleValue];seedY=[seedRecord[@"seedY"] doubleValue];
|
||||
} else scope=[NSString stringWithFormat:@"%@/%.9g/%.9g",key?:@"invalid",seedX,seedY];
|
||||
}
|
||||
NSDictionary *reference=nil;
|
||||
if(key) for(NSDictionary *record in _entries) {
|
||||
if(![record[@"context"] isEqual:context] || ![record[@"scope"] isEqual:scope])continue;
|
||||
if([record[@"key"] isEqual:key] && [record[@"raw"] isEqual:raw]) {[_mutex unlock];return record;}
|
||||
CMTime stored=PGRecordTime(record);
|
||||
// Same-time changed detections recompute from an earlier frame, never
|
||||
// feed an already smoothed result into another pass at that same time.
|
||||
if(CMTimeCompare(stored,time)>=0)continue;
|
||||
double dt=CMTimeGetSeconds(CMTimeSubtract(time,stored));
|
||||
if(dt<=.35 && (!reference || CMTimeCompare(stored,PGRecordTime(reference))>0))reference=record;
|
||||
}
|
||||
NSArray *prior=reference[@"output"];
|
||||
NSDictionary *previous=prior.count?prior.firstObject:nil;
|
||||
CGRect previousBox=PGArrayRect(previous[@"box"]);
|
||||
NSInteger index=-1;double best=-DBL_MAX;
|
||||
if(mode!=2) for(NSUInteger i=0;i<raw.count;i++) {
|
||||
CGRect box=PGArrayRect(raw[i][@"box"]);CGPoint center=PGCenter(box);double score;
|
||||
if(PGValidRect(previousBox)) {
|
||||
CGFloat factor=PGClamp(s.faceJumpGuardRange,1,4);
|
||||
CGRect guard=CGRectIntersection(CGRectMake(0,0,1,1),CGRectInset(previousBox,-previousBox.size.width*(factor-1)*.5,-previousBox.size.height*(factor-1)*.5));
|
||||
if(center.x<CGRectGetMinX(guard)||center.x>CGRectGetMaxX(guard)||center.y<CGRectGetMinY(guard)||center.y>CGRectGetMaxY(guard))continue;
|
||||
CGPoint p=PGCenter(previousBox);score=-((center.x-p.x)*(center.x-p.x)+(center.y-p.y)*(center.y-p.y));
|
||||
} else if(mode==1) score=-((center.x-seedX)*(center.x-seedX)+(center.y-seedY)*(center.y-seedY));
|
||||
else {CGRect clipped=CGRectIntersection(box,CGRectMake(0,0,1,1));CGPoint p=PGCenter(clipped);
|
||||
score=1.8*fmin(1,clipped.size.width*clipped.size.height)+1.1*(1-fmin(1,hypot(p.x-.5,p.y-.5)/.72));}
|
||||
if(index<0 || score>best) {index=(NSInteger)i;best=score;}
|
||||
}
|
||||
NSArray *output;BOOL held=NO;
|
||||
if(mode==2) output=raw;
|
||||
else if(index>=0) {
|
||||
NSDictionary *current=raw[(NSUInteger)index];
|
||||
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
|
||||
output=@[previous?PGStableGeometry(current,previous,dt,![reference[@"held"] boolValue]):current];
|
||||
} else {output=prior?:@[];held=output.count>0;}
|
||||
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
|
||||
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
|
||||
if(key) {
|
||||
NSData *serialized=[NSPropertyListSerialization dataWithPropertyList:record format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
NSUInteger bytes=serialized.length+512;
|
||||
record[@"bytes"]=@(bytes);
|
||||
// Replace changed raw geometry at the same timestamp rather than letting
|
||||
// callbacks grow history or make smoothing depend on redraw count.
|
||||
for(NSInteger i=(NSInteger)_entries.count-1;i>=0;i--) {
|
||||
NSDictionary *old=_entries[(NSUInteger)i];
|
||||
if([old[@"context"] isEqual:context] && [old[@"scope"] isEqual:scope] && [old[@"key"] isEqual:key]) {
|
||||
_estimatedBytes-=[old[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:(NSUInteger)i];
|
||||
}
|
||||
}
|
||||
if(bytes<=16*1024*1024) {[_entries addObject:[record copy]];_estimatedBytes+=bytes;}
|
||||
while(_entries.count>1200 || _estimatedBytes>16*1024*1024) {
|
||||
_estimatedBytes-=[_entries.firstObject[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:0];
|
||||
}
|
||||
}
|
||||
[_mutex unlock];return [record copy];
|
||||
}
|
||||
@end
|
||||
|
||||
@interface PGFaceProcessor () {
|
||||
NSLock *_lock;
|
||||
NSMutableArray<NSMutableDictionary<NSString *, NSDictionary *> *> *_targets;
|
||||
CIWarpKernel *_warp;
|
||||
CIColorKernel *_ellipse;
|
||||
CIColorKernel *_subtract;
|
||||
CIColorKernel *_box;
|
||||
PGRealtimeFaceTracker *_realtime;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGFaceProcessor
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lock = [[NSLock alloc] init];
|
||||
_realtime = [PGRealtimeFaceTracker new];
|
||||
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
||||
// Thin/V/chin/jaw/short reconstruct recovered local coordinate math.
|
||||
// Four other controls retain this project's analytic approximation.
|
||||
// Recovered V -> jaw -> chin -> short order follows the reference. Other
|
||||
// analytic body controls still precede V, so the full combined chain
|
||||
// with narrow/cheek/small-head/eyes is not claimed reference-equivalent.
|
||||
// Each pin changes the sampling coordinate, with only one image resample.
|
||||
NSMutableString *source = [NSMutableString stringWithString:
|
||||
@"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {"
|
||||
" vec2 direction=control.zw-control.xy; float radius=length(direction);"
|
||||
" if(radius<=0.0001) return q;"
|
||||
" float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);"
|
||||
" return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }"
|
||||
" vec2 pgJawPin(vec2 q, vec4 edge, vec2 anchor, float ratio) {"
|
||||
" float radius=length(edge.xy-edge.zw); if(radius<=0.0001) return q;"
|
||||
" float weight=clamp(1.0-length(q-edge.xy)/radius,0.0,1.0);"
|
||||
" return clamp(q-(anchor-edge.xy)*ratio*weight,vec2(0.0),vec2(1.0)); }"
|
||||
" vec2 pgShortEdge(vec2 q, vec2 a, vec2 b) {"
|
||||
" vec2 d=b-a; float squared=dot(d,d);"
|
||||
" float t=0.0; if(squared>0.000001) t=clamp(dot(q-a,d)/squared,0.0,1.0);"
|
||||
" float distance=length(q-(a+t*d)); float crossing=0.0;"
|
||||
" if((a.y>q.y && b.y<=q.y) || (b.y>q.y && a.y<=q.y)) {"
|
||||
" float dy=b.y-a.y; if(abs(dy)<=0.000001) dy=dy<0.0?-0.000001:0.000001;"
|
||||
" if(q.x<a.x+(b.x-a.x)*(q.y-a.y)/dy) crossing=1.0; }"
|
||||
" return vec2(distance,crossing); }"
|
||||
" kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortRatio, float narrow, float chinRatio, float vRatio, float jawRatio, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"];
|
||||
for (NSUInteger i=0; i<17; i++) [source appendFormat:@", vec4 pin%lu",(unsigned long)i];
|
||||
for (NSUInteger i=0; i<4; i++) [source appendFormat:@", vec4 vPin%lu",(unsigned long)i];
|
||||
for (NSUInteger i=0; i<3; i++) [source appendFormat:@", vec4 chinPin%lu",(unsigned long)i];
|
||||
[source appendString:@", vec2 jawAnchor, vec2 shortAxis"];
|
||||
for (NSUInteger i=0; i<6; i++) [source appendFormat:@", vec4 jawPin%lu",(unsigned long)i];
|
||||
for (NSUInteger i=0; i<11; i++) [source appendFormat:@", vec2 shortPoint%lu",(unsigned long)i];
|
||||
[source appendString:@") { vec2 p=destCoord();"
|
||||
" if(thinRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" vec2 n = (p-c)/size;"
|
||||
" float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));"
|
||||
" float sides = smoothstep(0.08,0.32,abs(n.x));"
|
||||
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
|
||||
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
|
||||
" p += (p-c)*smallHead*0.18*head;"
|
||||
" p.x += size.x*n.x*(narrow*0.30*body + cheek*0.18*cheekBand)*sides;"
|
||||
" if(vRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<4; i++) [source appendFormat:@" q=pgThinPin(q,vPin%lu,vRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" if(abs(jawRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<6; i++) [source appendFormat:@" q=pgJawPin(q,jawPin%lu,jawAnchor,jawRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" if(abs(chinRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<3; i++) [source appendFormat:@" q=pgThinPin(q,chinPin%lu,chinRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" if(abs(shortRatio)>0.000001 && length(shortAxis)>0.0001) {"
|
||||
" vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"
|
||||
" float nearest=10.0; float crossings=0.0;"];
|
||||
for (NSUInteger i=0; i<11; i++) [source appendFormat:@" { vec2 edge=pgShortEdge(q,shortPoint%lu,shortPoint%lu); nearest=min(nearest,edge.x); crossings+=edge.y; }",(unsigned long)i,(unsigned long)((i+1)%11)];
|
||||
[source appendString:@" float weight=mod(crossings,2.0)>0.5?1.0:clamp(1.0-nearest/length(shortAxis),0.0,1.0);"
|
||||
" q=clamp(q-shortAxis*shortRatio*weight,vec2(0.0),vec2(1.0));"
|
||||
" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
|
||||
" p -= (p-le)*bigEye*0.33*wl*wl;"
|
||||
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
|
||||
" p -= (p-re)*bigEye*0.33*wr*wr; return p; }"];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_warp = [CIWarpKernel kernelWithString:source];
|
||||
_ellipse = [CIColorKernel kernelWithString:@"kernel vec4 pgEllipse(vec2 c, vec2 r, float inner) { float v=1.0-smoothstep(inner,1.0,length((destCoord()-c)/max(r,vec2(0.001)))); return vec4(v,v,v,1.0); }"];
|
||||
_subtract = [CIColorKernel kernelWithString:@"kernel vec4 pgSubtract(__sample a, __sample b) { float v=max(0.0,a.r-b.r); return vec4(v,v,v,1.0); }"];
|
||||
_box = [CIColorKernel kernelWithString:@"kernel vec4 pgBox(vec4 b, float width, vec4 color) { vec2 p=destCoord(); float inside=step(b.x,p.x)*step(b.y,p.y)*step(p.x,b.z)*step(p.y,b.w); float edge=min(min(abs(p.x-b.x),abs(p.x-b.z)),min(abs(p.y-b.y),abs(p.y-b.w))); float a=inside*(1.0-smoothstep(width,width+1.0,edge)); return color*a; }"];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
CGFloat longest = MAX(extent.size.width, extent.size.height);
|
||||
CGFloat scale = MIN(1.0, (settings.fastMultiFace ? 720.0 : 1280.0) / longest);
|
||||
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
|
||||
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
||||
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
||||
request.revision = VNDetectFaceLandmarksRequestRevision3;
|
||||
request.constellation = VNRequestFaceLandmarksConstellation76Points;
|
||||
// An image request handler is intentionally local to one invocation. A shared
|
||||
// sequence handler would make parallel / reverse render order observable.
|
||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
||||
if (![handler performRequests:@[request] error:error]) return nil;
|
||||
NSArray<VNFaceObservation *> *faces = request.results ?: @[];
|
||||
return [faces sortedArrayUsingComparator:^NSComparisonResult(VNFaceObservation *a, VNFaceObservation *b) {
|
||||
CGRect x = a.boundingBox, y = b.boundingBox;
|
||||
if (CGRectGetMidX(x) != CGRectGetMidX(y)) return CGRectGetMidX(x) < CGRectGetMidX(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
if (CGRectGetMidY(x) != CGRectGetMidY(y)) return CGRectGetMidY(x) < CGRectGetMidY(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
return x.size.width > y.size.width ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)recordForTime:(CMTime)time target:(NSUInteger)target nearest:(BOOL)nearest {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!key || target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *record = _targets[target][key];
|
||||
if (nearest && [record[@"faces"] count] == 0) record = nil;
|
||||
if (!record && nearest) {
|
||||
// Pick a deterministic analyzed identity, retaining the last valid face
|
||||
// across lost detections. Only an explicit reset/reselection reacquires
|
||||
// a different identity; render ordering never updates this anchor.
|
||||
double distance = DBL_MAX;
|
||||
CMTime selectedTime = kCMTimeInvalid;
|
||||
for (NSDictionary *candidate in _targets[target].allValues) {
|
||||
if ([candidate[@"faces"] count] == 0) continue;
|
||||
CMTime stored = CMTimeMakeWithEpoch([candidate[@"value"] longLongValue], [candidate[@"scale"] intValue], [candidate[@"epoch"] longLongValue]);
|
||||
if (stored.epoch != time.epoch) continue;
|
||||
double delta = fabs(CMTimeGetSeconds(CMTimeSubtract(stored, time)));
|
||||
if (delta < distance || (delta == distance && CMTIME_IS_VALID(selectedTime) && CMTimeCompare(stored, selectedTime) < 0)) {
|
||||
record = candidate; distance = delta; selectedTime = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
return record;
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary *> *)geometryForObservations:(NSArray<VNFaceObservation *> *)observations
|
||||
settings:(PGBeautySettings)settings previous:(NSDictionary *)previous {
|
||||
BOOL selectOne = settings.singleFace || settings.trackingEnabled;
|
||||
if (!selectOne) {
|
||||
NSMutableArray *all = [NSMutableArray array];
|
||||
for (VNFaceObservation *face in observations) if (PGValidRect(face.boundingBox) && all.count < 16) [all addObject:PGGeometry(face)];
|
||||
return all;
|
||||
}
|
||||
CGPoint selection = CGPointMake(PGClamp(settings.selectionX, 0, 1), PGClamp(settings.selectionY, 0, 1));
|
||||
NSArray *previousFaces = previous[@"faces"];
|
||||
CGRect prior = previousFaces.count ? PGArrayRect(previousFaces.firstObject[@"box"]) : CGRectZero;
|
||||
BOOL guard = settings.faceJumpGuardRange > 0 && PGValidRect(prior);
|
||||
CGPoint reference = guard ? PGCenter(prior) : selection;
|
||||
VNFaceObservation *best = nil;
|
||||
double bestScore = DBL_MAX;
|
||||
for (VNFaceObservation *face in observations) {
|
||||
CGRect box = face.boundingBox;
|
||||
if (!PGValidRect(box)) continue;
|
||||
CGPoint center = PGCenter(box);
|
||||
double dx = (center.x - reference.x), dy = (center.y - reference.y);
|
||||
if (guard) {
|
||||
double radius = MAX(prior.size.width, prior.size.height) * PGClamp(settings.faceJumpGuardRange, 0.1, 8);
|
||||
double ratio = box.size.width / prior.size.width;
|
||||
if (hypot(dx, dy) > radius || ratio < 0.4 || ratio > 2.5) continue;
|
||||
}
|
||||
double score = dx * dx + dy * dy;
|
||||
if (!guard && CGRectContainsPoint(box, selection)) score -= 2;
|
||||
if (score < bestScore) { best = face; bestScore = score; }
|
||||
}
|
||||
return best ? @[PGGeometry(best)] : @[];
|
||||
}
|
||||
|
||||
- (BOOL)analyzeImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!image || !key || CGRectIsInfinite(image.extent) || CGRectIsEmpty(image.extent)) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Face analysis requires a finite image and valid frame time."}];
|
||||
return NO;
|
||||
}
|
||||
NSArray *observations = [self observationsInImage:image settings:settings error:error];
|
||||
if (!observations) return NO;
|
||||
NSUInteger target = MIN((NSUInteger)settings.targetIndex, 2);
|
||||
NSDictionary *prior = [self recordForTime:time target:target nearest:YES];
|
||||
NSArray *geometry = [self geometryForObservations:observations settings:settings previous:prior];
|
||||
NSDictionary *record = @{@"value":@(time.value), @"scale":@(time.timescale), @"epoch":@(time.epoch), @"faces":geometry};
|
||||
[_lock lock];
|
||||
_targets[target][key] = record;
|
||||
[_lock unlock];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CIImage *)ellipseInRect:(CGRect)rect extent:(CGRect)extent inner:(CGFloat)inner {
|
||||
return [_ellipse applyWithExtent:extent arguments:@[PGVector(PGCenter(rect)), [CIVector vectorWithX:rect.size.width * 0.5 Y:rect.size.height * 0.5], @(inner)]];
|
||||
}
|
||||
|
||||
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent {
|
||||
if (!(s.faceThin || s.faceBigEye || s.faceShort || s.faceNarrow || s.faceChin || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead)) return [image imageByCroppingToRect:extent];
|
||||
CIImage *result = image;
|
||||
for (NSDictionary *face in geometry) {
|
||||
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
|
||||
NSArray *points76 = PGSanitizedPoints76(face[@"points76"]);
|
||||
float thinRatio = 0, chinRatio = 0, jawRatio = 0, shortRatio = 0, vRatio = 0;
|
||||
if (points76) {
|
||||
double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width,
|
||||
([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height);
|
||||
double midX = ([points76[74][0] doubleValue]+[points76[60][0] doubleValue])*.5;
|
||||
double midY = ([points76[74][1] doubleValue]+[points76[60][1] doubleValue])*.5;
|
||||
double height = 2*hypot(([points76[67][0] doubleValue]-midX)*extent.size.width,
|
||||
([points76[67][1] doubleValue]-midY)*extent.size.height);
|
||||
double gain = fmin(2.25,fmax(1,280/fmax(1,fmax(width,height))));
|
||||
thinRatio = PGClamp(s.faceThin,0,1)*.025*gain;
|
||||
float chin = PGClamp(s.faceChin,-1,1);
|
||||
chinRatio = chin*(chin>0 ? .04 : .02)*gain;
|
||||
jawRatio = PGClamp(s.faceJaw,0,1)*.05*gain;
|
||||
shortRatio = PGClamp(s.faceShort,-1,1)*.25*gain;
|
||||
// The specified 2.4.0 shader takes the CPU-scaled .065 V ratio
|
||||
// directly; it no longer applies the older shader's size gain.
|
||||
vRatio = PGClamp(s.faceV,0,1)*.065;
|
||||
}
|
||||
BOOL otherControls = s.faceBigEye || s.faceNarrow || s.faceCheekbone || s.faceSmallHead;
|
||||
// No full topology means no guessed bbox thin/V/chin/jaw/short deformation. Legacy
|
||||
// archives using only those controls remain exact image/mask pass-through.
|
||||
if (thinRatio <= .000001 && vRatio <= .000001 && fabsf(chinRatio) <= .000001 && fabsf(jawRatio) <= .000001 && fabsf(shortRatio) <= .000001 && !otherControls) continue;
|
||||
CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15));
|
||||
CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15));
|
||||
CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent));
|
||||
NSMutableArray *arguments = [@[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height],
|
||||
PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)],
|
||||
@(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(shortRatio),
|
||||
@(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(vRatio),
|
||||
@(jawRatio), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
|
||||
[CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy];
|
||||
for (NSUInteger i=0; i<17; i++) {
|
||||
double ox=0,oy=0,ax=0,ay=0;
|
||||
if (points76) {
|
||||
NSArray *origin=points76[PGThinPins[i][0]],*anchor=points76[PGThinPins[i][1]];
|
||||
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
|
||||
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
|
||||
}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
|
||||
}
|
||||
for (NSUInteger i=0; i<4; i++) {
|
||||
double ox=0,oy=0,ax=0,ay=0;
|
||||
if (points76) {
|
||||
NSArray *origin=points76[PGVFacePins[i][0]],*anchor=points76[PGVFacePins[i][1]];
|
||||
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
|
||||
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
|
||||
}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
|
||||
}
|
||||
for (NSUInteger i=0; i<3; i++) {
|
||||
double ox=0,oy=0,ax=0,ay=0;
|
||||
if (points76) {
|
||||
NSArray *origin=points76[PGChinPins[i][0]],*anchor=points76[PGChinPins[i][1]];
|
||||
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
|
||||
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
|
||||
}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
|
||||
}
|
||||
CGPoint anchor=CGPointZero,axis=CGPointZero;
|
||||
if(points76){
|
||||
anchor=CGPointMake([points76[29][0]doubleValue],1-[points76[29][1]doubleValue]);
|
||||
axis=CGPointMake([points76[46][0]doubleValue]-[points76[48][0]doubleValue],
|
||||
[points76[48][1]doubleValue]-[points76[46][1]doubleValue]);
|
||||
}
|
||||
[arguments addObject:PGVector(anchor)];[arguments addObject:PGVector(axis)];
|
||||
for(NSUInteger i=0;i<6;i++){
|
||||
double ox=0,oy=0,nx=0,ny=0;
|
||||
if(points76){NSArray *origin=points76[PGJawPins[i][0]],*neighbour=points76[PGJawPins[i][1]];
|
||||
ox=[origin[0]doubleValue];oy=1-[origin[1]doubleValue];nx=[neighbour[0]doubleValue];ny=1-[neighbour[1]doubleValue];}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:nx W:ny]];
|
||||
}
|
||||
for(NSUInteger i=62;i<=72;i++){
|
||||
CGPoint point=CGPointZero;if(points76)point=CGPointMake([points76[i][0]doubleValue],1-[points76[i][1]doubleValue]);
|
||||
[arguments addObject:PGVector(point)];
|
||||
}
|
||||
CIImage *input = [result imageByClampingToExtent];
|
||||
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
|
||||
if (!result) return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
}
|
||||
|
||||
- (PGFaceFrame *)processImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsInfinite(extent) || CGRectIsEmpty(extent) || !_ellipse || !_warp || !_box || !_subtract) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:2 userInfo:@{NSLocalizedDescriptionKey:@"Unable to initialize face processing for this image."}];
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *record = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:NO] : nil;
|
||||
NSArray<NSDictionary *> *geometry,*appearance;
|
||||
NSArray<VNFaceObservation *> *observations;
|
||||
if (record) {
|
||||
geometry = record[@"faces"];
|
||||
appearance = geometry;
|
||||
NSMutableArray *restored = [NSMutableArray array];
|
||||
for (NSDictionary *face in geometry) [restored addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||
observations = restored;
|
||||
} else {
|
||||
NSArray *detected = [self observationsInImage:image settings:settings error:error];
|
||||
if (!detected) return nil;
|
||||
if(settings.trackingEnabled) {
|
||||
// Preserve the explicit offline-analysis contract. Its stored
|
||||
// identity fallback never reads or writes realtime temporal state.
|
||||
NSDictionary *anchor=[self recordForTime:time target:MIN((NSUInteger)settings.targetIndex,2) nearest:YES];
|
||||
geometry=[self geometryForObservations:detected settings:settings previous:anchor];
|
||||
appearance=geometry;
|
||||
NSMutableArray *selected=[NSMutableArray new];
|
||||
for(NSDictionary *face in geometry) for(VNFaceObservation *observation in detected)
|
||||
if(CGRectEqualToRect(observation.boundingBox,PGArrayRect(face[@"box"]))) {[selected addObject:observation];break;}
|
||||
observations=selected;
|
||||
} else {
|
||||
NSMutableArray *raw=[NSMutableArray new],*validObservations=[NSMutableArray new];
|
||||
for(VNFaceObservation *observation in detected) if(PGValidRect(observation.boundingBox) && raw.count<16) {
|
||||
[raw addObject:PGGeometry(observation)];[validObservations addObject:observation];
|
||||
}
|
||||
NSDictionary *live=[_realtime resultForGeometry:raw settings:settings time:time extent:extent];
|
||||
geometry=live[@"output"];
|
||||
if(settings.singleFace && !settings.fastMultiFace) {
|
||||
NSInteger index=[live[@"index"] integerValue];
|
||||
if(index>=0 && (NSUInteger)index<validObservations.count) {
|
||||
// Retain the selected original Vision observation. A stable
|
||||
// geometry box need not equal its unsmoothed detection box.
|
||||
observations=@[validObservations[(NSUInteger)index]];
|
||||
appearance=@[raw[(NSUInteger)index]];
|
||||
} else {
|
||||
appearance=geometry;
|
||||
NSMutableArray *held=[NSMutableArray new];
|
||||
for(NSDictionary *face in geometry) [held addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||
observations=held;
|
||||
}
|
||||
} else {
|
||||
// Default single-primary geometry does not narrow the separate
|
||||
// skin/teeth/eyes support, which still covers the detected group.
|
||||
appearance=raw;observations=validObservations;
|
||||
}
|
||||
}
|
||||
}
|
||||
CIImage *skin = PGBlack(extent), *teeth = PGBlack(extent), *eyes = PGBlack(extent), *protection = PGBlack(extent), *overlay = PGTransparent(extent);
|
||||
for (NSDictionary *face in appearance) {
|
||||
CGRect box = PGArrayRect(face[@"box"]);
|
||||
CGRect skinRect = PGInFace(box, -0.10, -0.04, 1.20, 1.16);
|
||||
skin = PGMaximum(skin, [self ellipseInRect:PGToPixels(skinRect, extent) extent:extent inner:0.78]);
|
||||
NSArray *names = @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"lips", @"nose"];
|
||||
NSArray<NSValue *> *fallbacks = @[[NSValue valueWithRect:PGInFace(box, 0.16, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.12, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.27, 0.16, 0.46, 0.18)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.39, 0.33, 0.22, 0.16)]];
|
||||
for (NSUInteger i = 0; i < names.count; i++) {
|
||||
CGRect region = PGToPixels(PGRegionBounds(face, names[i], fallbacks[i].rectValue), extent);
|
||||
CGRect expanded = CGRectInset(region, -region.size.width * 0.15, -region.size.height * 0.35);
|
||||
CIImage *mask = [self ellipseInRect:expanded extent:extent inner:0.45];
|
||||
protection = PGMaximum(protection, mask);
|
||||
if (i < 2) eyes = PGMaximum(eyes, [self ellipseInRect:region extent:extent inner:0.45]);
|
||||
}
|
||||
CGRect mouth = PGRegionBounds(face, @"mouth", PGInFace(box, 0.30, 0.20, 0.40, 0.08));
|
||||
teeth = PGMaximum(teeth, [self ellipseInRect:PGToPixels(mouth, extent) extent:extent inner:0.35]);
|
||||
if (settings.showFaceBoxes) {
|
||||
CGRect pixels = PGToPixels(box, extent);
|
||||
CGFloat inset = MAX(1, MIN(pixels.size.width, pixels.size.height) * 0.005);
|
||||
CIImage *outline = [_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)], @(inset), [CIVector vectorWithX:0.12 Y:1.0 Z:0.35 W:0.95]]];
|
||||
overlay = [outline imageByCompositingOverImage:overlay];
|
||||
}
|
||||
}
|
||||
if(settings.showTrackBox) for(NSDictionary *face in geometry) {
|
||||
CGRect pixels=PGToPixels(PGArrayRect(face[@"box"]),extent);
|
||||
CGFloat inset=MAX(1,MIN(pixels.size.width,pixels.size.height)*.005);
|
||||
CIImage *outline=[_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)],@(inset),[CIVector vectorWithX:1 Y:.8 Z:.2 W:.95]]];
|
||||
overlay=[outline imageByCompositingOverImage:overlay];
|
||||
}
|
||||
CIImage *colorCoverage=skin;
|
||||
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
|
||||
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
|
||||
frame.faces = observations ?: @[];
|
||||
frame.deformationGeometry=geometry?:@[];
|
||||
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
|
||||
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
|
||||
frame.colorMask = [self warpImage:colorCoverage geometry:geometry settings:settings extent:extent];
|
||||
frame.teethMask = [self warpImage:teeth geometry:geometry settings:settings extent:extent];
|
||||
frame.eyeMask = [self warpImage:eyes geometry:geometry settings:settings extent:extent];
|
||||
frame.overlayImage = [overlay imageByCroppingToRect:extent];
|
||||
if (!frame.image || !frame.skinMask || !frame.colorMask || !frame.teethMask || !frame.eyeMask) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:3 userInfo:@{NSLocalizedDescriptionKey:@"Unable to apply face geometry."}];
|
||||
return nil;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (void)resetTracking {
|
||||
[_lock lock];
|
||||
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
||||
[_lock unlock];
|
||||
[_realtime reset];
|
||||
}
|
||||
|
||||
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
||||
if (target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *frames = [_targets[target] copy];
|
||||
[_lock unlock];
|
||||
if (!frames.count) return nil;
|
||||
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1, @"frames":frames} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
|
||||
- (void)setTrackingData:(NSData *)data forTarget:(NSUInteger)target {
|
||||
if (target >= 3) return;
|
||||
NSMutableDictionary *frames = [NSMutableDictionary dictionary];
|
||||
if (data.length && data.length <= 128 * 1024 * 1024) {
|
||||
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
|
||||
if ([archive isKindOfClass:NSDictionary.class] && [archive[@"version"] isEqual:@1] && [archive[@"frames"] isKindOfClass:NSDictionary.class] && [archive[@"frames"] count] <= 250000) {
|
||||
for (id key in archive[@"frames"]) {
|
||||
id raw = archive[@"frames"][key];
|
||||
if (![key isKindOfClass:NSString.class] || ![raw isKindOfClass:NSDictionary.class]) continue;
|
||||
NSNumber *value = raw[@"value"], *scale = raw[@"scale"], *epoch = raw[@"epoch"];
|
||||
if (![value isKindOfClass:NSNumber.class] || ![scale isKindOfClass:NSNumber.class] || ![epoch isKindOfClass:NSNumber.class] || scale.intValue <= 0) continue;
|
||||
CMTime time = CMTimeMakeWithEpoch(value.longLongValue, scale.intValue, epoch.longLongValue);
|
||||
if (![key isEqualToString:PGTimeKey(time)] || ![raw[@"faces"] isKindOfClass:NSArray.class] || [raw[@"faces"] count] > 16) continue;
|
||||
NSMutableArray *faces = [NSMutableArray array];
|
||||
BOOL valid = YES;
|
||||
for (id geometry in raw[@"faces"]) {
|
||||
NSDictionary *face = PGSanitizedGeometry(geometry);
|
||||
if (!face) { valid = NO; break; }
|
||||
[faces addObject:face];
|
||||
}
|
||||
if (valid) frames[key] = @{@"value":value, @"scale":scale, @"epoch":epoch, @"faces":[faces copy]};
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock lock];
|
||||
_targets[target] = frames;
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,170 @@
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import "PGParameterIDs.h"
|
||||
#include <math.h>
|
||||
|
||||
@interface PGFaceSelectOSC : NSObject <FxOnScreenControl_v4>
|
||||
@property (assign) id<PROAPIAccessing> apiManager;
|
||||
@property BOOL dragging;
|
||||
@property BOOL undoOpen;
|
||||
@property BOOL pointSetterUsesPixels;
|
||||
@property BOOL loggedCoordinates;
|
||||
@property BOOL loggedPointSetter;
|
||||
@end
|
||||
@implementation PGFaceSelectOSC
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)api {
|
||||
if((self=[super init])) _apiManager=api; return self;
|
||||
}
|
||||
- (FxDrawingCoordinates)drawingCoordinates {return kFxDrawingCoordinates_CANVAS;}
|
||||
- (id<FxOnScreenControlAPI_v4>)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];}
|
||||
- (BOOL)objectPointFromCanvasX:(double)x y:(double)y objectX:(double *)objectX objectY:(double *)objectY {
|
||||
id<FxOnScreenControlAPI_v4> api=[self oscAPI];
|
||||
if(!api || !objectX || !objectY || !isfinite(x) || !isfinite(y)) return NO;
|
||||
// Request CANVAS events explicitly, then let the host invert its current
|
||||
// zoom, pan and object transform. The conversion API has no success result;
|
||||
// untouched or nonfinite outputs must never become a parameter write.
|
||||
double nx=NAN,ny=NAN;
|
||||
[api convertPointFromSpace:kFxDrawingCoordinates_CANVAS fromX:x fromY:y
|
||||
toSpace:kFxDrawingCoordinates_OBJECT toX:&nx toY:&ny];
|
||||
if(!isfinite(nx) || !isfinite(ny)) return NO;
|
||||
*objectX=nx;*objectY=ny;return YES;
|
||||
}
|
||||
- (BOOL)selectionEnabledAtTime:(CMTime)time {
|
||||
BOOL selected=NO;id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
return api && [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time] && selected;
|
||||
}
|
||||
- (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time {
|
||||
if(!tile.ioSurface||width<=0||height<=0) return;
|
||||
size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
|
||||
if(!surfaceW||!surfaceH) return;
|
||||
CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,surfaceW,surfaceH)];
|
||||
if([self selectionEnabledAtTime:time] && [self oscAPI]) {
|
||||
double x=.5,y=.5;
|
||||
id<FxParameterRetrievalAPI_v6> params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
BOOL valid=[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time] && isfinite(x) && isfinite(y);
|
||||
double canvasX=NAN,canvasY=NAN;
|
||||
// FxPlug OBJECT space is normalized 0..1, exactly like point parameters.
|
||||
// objectBounds is a pixel/document rectangle and must not be applied here.
|
||||
if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:x fromY:y toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
|
||||
if(!_loggedCoordinates) {
|
||||
_loggedCoordinates=YES;
|
||||
CGRect objectBounds=[[self oscAPI] objectBounds];
|
||||
NSLog(@"PGFaceOSC coordinates selection=(%.9g,%.9g) objectBounds=(%.9g,%.9g,%.9g,%.9g) canvas=(%.9g,%.9g) surface=%zux%zu origin=%lu",
|
||||
x,y,objectBounds.origin.x,objectBounds.origin.y,objectBounds.size.width,objectBounds.size.height,
|
||||
canvasX,canvasY,surfaceW,surfaceH,(unsigned long)tile.imageOrigin);
|
||||
}
|
||||
// CANVAS coordinates address the OSC surface. Callback width/height can
|
||||
// describe the effect image and must not rescale the converted point.
|
||||
double backing=[[self oscAPI] backingScaleFactor];
|
||||
CGFloat scale=isfinite(backing)?MAX(1,backing):1;
|
||||
CGFloat radius=10*scale;
|
||||
if(valid && isfinite(canvasX) && isfinite(canvasY)) {
|
||||
CIColor *color=part?[CIColor colorWithRed:1 green:.8 blue:.2]:[CIColor colorWithRed:.1 green:.9 blue:.8];
|
||||
CIImage *ring=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-2),@"inputRadius1":@(radius),@"inputColor0":color,@"inputColor1":CIColor.clearColor}].outputImage;
|
||||
CIImage *hole=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-3),@"inputRadius1":@(radius-2),@"inputColor0":CIColor.blackColor,@"inputColor1":CIColor.whiteColor}].outputImage;
|
||||
ring=[ring imageByApplyingFilter:@"CIBlendWithMask" withInputParameters:@{kCIInputBackgroundImageKey:output,kCIInputMaskImageKey:hole}];
|
||||
CIImage *v=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-scale,canvasY-16*scale,2*scale,32*scale)];
|
||||
CIImage *h=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-16*scale,canvasY-scale,32*scale,2*scale)];
|
||||
output=[[h imageByCompositingOverImage:v] imageByCompositingOverImage:ring];
|
||||
}
|
||||
}
|
||||
CIContext *context=[CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
||||
size_t w=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),h=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
|
||||
if(tile.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,h)];
|
||||
[context render:output toIOSurface:(__bridge IOSurfaceRef)tile.ioSurface bounds:CGRectMake(0,0,w,h) colorSpace:tile.colorSpace];
|
||||
}
|
||||
- (void)hitTestOSCAtMousePositionX:(double)x mousePositionY:(double)y activePart:(NSInteger *)part atTime:(CMTime)time {
|
||||
if(!part) return;*part=0;
|
||||
double nx=NAN,ny=NAN;
|
||||
if([self selectionEnabledAtTime:time] && [self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny] &&
|
||||
nx>=0 && nx<=1 && ny>=0 && ny<=1) *part=1;
|
||||
}
|
||||
- (BOOL)pointWrite:(id<FxParameterSettingAPI_v5>)setter x:(double)x y:(double)y time:(CMTime)time {
|
||||
return setter && isfinite(x) && isfinite(y) && [setter setXValue:x YValue:y toParameter:PGSelection atTime:time];
|
||||
}
|
||||
- (BOOL)readPoint:(id<FxParameterRetrievalAPI_v6>)reader x:(double *)x y:(double *)y time:(CMTime)time {
|
||||
return reader && [reader getXValue:x YValue:y fromParameter:PGSelection atTime:time] && isfinite(*x) && isfinite(*y);
|
||||
}
|
||||
- (BOOL)pointPixelSize:(CGSize *)size {
|
||||
id<FxOnScreenControlAPI_v4> api=[self oscAPI];if(!api || !size)return NO;
|
||||
CGRect bounds=[api objectBounds];
|
||||
if(!isfinite(bounds.size.width)||!isfinite(bounds.size.height)||bounds.size.width<=4||bounds.size.height<=4)return NO;
|
||||
*size=bounds.size;return YES;
|
||||
}
|
||||
- (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update {
|
||||
if(update) *update=NO;
|
||||
if(!isfinite(x)||!isfinite(y)) return;
|
||||
double nx=fmin(1,fmax(0,x)),ny=fmin(1,fmax(0,y));
|
||||
id<FxParameterSettingAPI_v5> setter=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
id<FxParameterRetrievalAPI_v6> reader=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
double previousX=NAN,previousY=NAN;
|
||||
if(!setter || ![self readPoint:reader x:&previousX y:&previousY time:time])return;
|
||||
CGSize pixels=CGSizeZero;
|
||||
if(_pointSetterUsesPixels && ![self pointPixelSize:&pixels])return;
|
||||
double rawX=_pointSetterUsesPixels ? (nx-.5)*pixels.width : nx;
|
||||
double rawY=_pointSetterUsesPixels ? (ny-.5)*pixels.height : ny;
|
||||
BOOL accepted=[self pointWrite:setter x:rawX y:rawY time:time];
|
||||
BOOL changed=accepted,pixelUnits=_pointSetterUsesPixels;
|
||||
double actualX=NAN,actualY=NAN;
|
||||
BOOL readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time];
|
||||
const double tolerance=0.00001;
|
||||
BOOL verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance;
|
||||
// Motion's FxPlug 4 bridge can normalize point reads while its setter still
|
||||
// writes centered pixel values directly to OZChannel2D. Detect that exact
|
||||
// response instead of choosing units from the app version or coordinate size.
|
||||
if(readBack && !verified && !_pointSetterUsesPixels && [self pointPixelSize:&pixels] &&
|
||||
fabs(actualX-(.5+nx/pixels.width))<=tolerance &&
|
||||
fabs(actualY-(.5+ny/pixels.height))<=tolerance) {
|
||||
pixelUnits=YES;
|
||||
rawX=(nx-.5)*pixels.width;rawY=(ny-.5)*pixels.height;
|
||||
accepted=[self pointWrite:setter x:rawX y:rawY time:time];changed=changed||accepted;
|
||||
actualX=actualY=NAN;
|
||||
readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time];
|
||||
verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance;
|
||||
if(verified)_pointSetterUsesPixels=YES;
|
||||
}
|
||||
if(!verified && changed) {
|
||||
// Restore the previous point when verification fails. A failure must not
|
||||
// look like a successful drag or silently retain the calibration point.
|
||||
double restoreX=pixelUnits ? (previousX-.5)*pixels.width : previousX;
|
||||
double restoreY=pixelUnits ? (previousY-.5)*pixels.height : previousY;
|
||||
[self pointWrite:setter x:restoreX y:restoreY time:time];
|
||||
}
|
||||
if(!_loggedPointSetter) {
|
||||
_loggedPointSetter=YES;
|
||||
NSLog(@"PGFaceOSC point-write desired=(%.9g,%.9g) setter=(%.9g,%.9g) readback=(%.9g,%.9g) mode=%@ verified=%d",
|
||||
nx,ny,rawX,rawY,actualX,actualY,_pointSetterUsesPixels?@"centered-pixels":@"normalized",verified);
|
||||
}
|
||||
if(update)*update=verified;
|
||||
}
|
||||
- (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(part!=1 || ![self selectionEnabledAtTime:time] ||
|
||||
![self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny] || nx<0 || nx>1 || ny<0 || ny>1) return;
|
||||
id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];
|
||||
if(_undoOpen) [undo endUndoGroup];
|
||||
_undoOpen=[undo startUndoGroup:@"选择人脸"];
|
||||
BOOL changed=NO;[self setSelectionX:nx y:ny time:time update:&changed];
|
||||
_dragging=changed;
|
||||
if(!changed && _undoOpen) {[undo endUndoGroup];_undoOpen=NO;}
|
||||
if(update) *update=changed;
|
||||
}
|
||||
- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(_dragging && part==1 && [self selectionEnabledAtTime:time] &&
|
||||
[self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny]) [self setSelectionX:nx y:ny time:time update:update];
|
||||
}
|
||||
- (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(_dragging && part==1 && [self selectionEnabledAtTime:time] &&
|
||||
[self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny]) [self setSelectionX:nx y:ny time:time update:update];
|
||||
_dragging=NO;
|
||||
if(_undoOpen) {id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];_undoOpen=NO;}
|
||||
}
|
||||
- (void)keyDownAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
|
||||
- (void)keyUpAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
#import "PGParameterIDs.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// The descriptor list is shared by UI, persistence and render settings.
|
||||
/// Keys: id, name, section (smooth/color/shape/more), kind
|
||||
/// (slider/toggle/preset/master), min, max, default, format, bypass.
|
||||
FOUNDATION_EXPORT NSArray<NSDictionary *> *PGInspectorControls(void);
|
||||
FOUNDATION_EXPORT NSDictionary * _Nullable PGInspectorDescriptor(UInt32 identifier);
|
||||
|
||||
@interface PGInspectorState : NSObject <NSSecureCoding, NSCopying, FxCustomParameterInterpolation_v2>
|
||||
@property (nonatomic, copy, readonly) NSDictionary<NSNumber *, NSNumber *> *values;
|
||||
@property (nonatomic, copy, readonly) NSSet<NSNumber *> *disabledParameters;
|
||||
/// An untouched panel must not replace parameters from an existing document.
|
||||
@property (nonatomic, readonly) BOOL hasEdits;
|
||||
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited;
|
||||
- (double)valueForParameter:(UInt32)identifier;
|
||||
- (BOOL)isParameterEnabled:(UInt32)identifier;
|
||||
@end
|
||||
|
||||
FOUNDATION_EXPORT PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings settings, PGInspectorState * _Nullable state);
|
||||
|
||||
/// All arbitrary-time FxPlug parameter access is bracketed by host actions.
|
||||
/// The published custom object owns the full UI state, so FCP save/undo does
|
||||
/// not depend on unpublished parameters being mirrored by the template.
|
||||
@interface PGInspectorController : NSObject
|
||||
@property (nonatomic, strong, readonly) PGInspectorState *state;
|
||||
@property (nonatomic, readonly) BOOL available;
|
||||
@property (nonatomic, copy, readonly) NSString *statusMessage;
|
||||
/// The actual custom inspector view identifies the action to the native host.
|
||||
@property (nonatomic, weak, nullable) NSView *actionSender;
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
|
||||
- (void)refresh;
|
||||
- (void)documentDidChange;
|
||||
- (void)beginInteraction;
|
||||
- (void)endInteraction;
|
||||
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier;
|
||||
- (BOOL)toggleEnabledForParameter:(UInt32)identifier;
|
||||
- (BOOL)resetParameter:(UInt32)identifier;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,291 @@
|
||||
#import "PGInspectorState.h"
|
||||
#import <math.h>
|
||||
#import <stddef.h>
|
||||
#import <os/log.h>
|
||||
|
||||
NSArray<NSDictionary *> *PGInspectorControls(void) {
|
||||
static NSArray *controls; static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
PGBeautySettings defaults=PGBeautyDefaults();
|
||||
NSMutableArray *items=[NSMutableArray new];
|
||||
#define ITEM(ID,NAME,FIELD,SECTION,KIND,FORMAT,MIN,MAX,BYPASS,STORAGE) \
|
||||
[items addObject:@{@"id":@(ID),@"name":@NAME,@"section":@SECTION,@"kind":@KIND,@"format":@FORMAT,@"min":@(MIN),@"max":@(MAX),@"default":@(defaults.FIELD),@"bypass":@(BYPASS),@"offset":@(offsetof(PGBeautySettings,FIELD)),@"storage":@STORAGE}]
|
||||
ITEM(PGSmoothingEnabled,"磨皮",featureProtection,"smooth","master","integer",0,1,NO,"master");
|
||||
ITEM(PGMidtone,"磨皮程度",midtoneDetail,"smooth","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGMicroDetail,"微细锐化",microDetail,"smooth","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGFeatureProtectionAmount,"五官保护",featureProtectionAmount,"smooth","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGNonSkinProtection,"非肤色区保护",nonSkinProtection,"smooth","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGColorEnabled,"美白",featureProtection,"color","master","integer",0,1,NO,"master");
|
||||
ITEM(PGTonePreset,"肤色",skinTonePreset,"color","preset","integer",0,6,NO,"uint");
|
||||
NSMutableDictionary *preset=[items.lastObject mutableCopy];
|
||||
preset[@"options"]=@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"];items[items.count-1]=preset;
|
||||
ITEM(PGBrightness,"肤色亮度",skinBrightness,"color","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGTeeth,"白牙",teethWhitening,"color","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGHue,"皮肤色相",skinHue,"color","slider","degrees",-30,30,YES,"float");
|
||||
ITEM(PGShapeEnabled,"瘦脸",featureProtection,"shape","master","integer",0,1,NO,"master");
|
||||
ITEM(PGTrackBoxes,"检测框预览",showTrackBox,"shape","toggle","integer",0,1,NO,"uint");
|
||||
ITEM(PGFastMulti,"多人模式",fastMultiFace,"shape","toggle","integer",0,1,NO,"uint");
|
||||
NSMutableDictionary *multi=[items.lastObject mutableCopy];
|
||||
multi[@"tooltip"]=@"多人模式易抖动,请谨慎使用";items[items.count-1]=multi;
|
||||
ITEM(PGThin,"瘦脸",faceThin,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGBigEye,"大眼",faceBigEye,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGShort,"短脸",faceShort,"shape","slider","signed",-1,1,YES,"float");
|
||||
ITEM(PGNarrow,"窄脸",faceNarrow,"shape","slider","signed",-1,1,YES,"float");
|
||||
ITEM(PGChin,"下巴",faceChin,"shape","slider","signed",-1,1,YES,"float");
|
||||
ITEM(PGVFace,"V脸",faceV,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGJaw,"下颌骨",faceJaw,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGCheekbone,"颧骨",faceCheekbone,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGSmallHead,"小头",faceSmallHead,"shape","slider","signed",0,1,YES,"float");
|
||||
ITEM(PGSmallHeadRange,"小头范围",faceSmallHeadRange,"shape","slider","percent",0,1,NO,"float");
|
||||
ITEM(PGAmount,"磨皮强度",amount,"more","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGHighPass,"全局磨皮",highPassDetail,"more","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGRadius,"磨皮半径",radius,"more","slider","radius",1,30,NO,"float");
|
||||
ITEM(PGSaturation,"肤色饱和度",skinSaturation,"more","slider","percent",-100,100,YES,"float");
|
||||
ITEM(PGEvenTone,"均匀肤色",skinEvenTone,"more","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGEyeBright,"眼睛提亮",eyeBrightening,"more","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGSharpness,"锐化",sharpness,"more","slider","percent",0,1,YES,"float");
|
||||
ITEM(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,"more","slider","percent",1,4,NO,"float");
|
||||
ITEM(PGMix,"整体混合",mix,"more","slider","percent",0,1,NO,"float");
|
||||
#undef ITEM
|
||||
controls=[items copy];
|
||||
}); return controls;
|
||||
}
|
||||
NSDictionary *PGInspectorDescriptor(UInt32 identifier) {
|
||||
for(NSDictionary *d in PGInspectorControls()) if([d[@"id"] unsignedIntValue]==identifier) return d;
|
||||
return nil;
|
||||
}
|
||||
static double PGControlValue(id raw,NSDictionary *d) {
|
||||
double value=[raw isKindOfClass:NSNumber.class]?[raw doubleValue]:[d[@"default"] doubleValue];
|
||||
if(!isfinite(value)) value=[d[@"default"] doubleValue];
|
||||
value=fmin([d[@"max"] doubleValue],fmax([d[@"min"] doubleValue],value));
|
||||
if(![d[@"storage"] isEqual:@"float"]) value=round(value);
|
||||
return value;
|
||||
}
|
||||
@implementation PGInspectorState
|
||||
+ (BOOL)supportsSecureCoding {return YES;}
|
||||
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited {
|
||||
PGInspectorState *state=[self new]; NSMutableDictionary *safe=[NSMutableDictionary new];NSMutableSet *bypassed=[NSMutableSet new];
|
||||
for(NSDictionary *d in PGInspectorControls()) {
|
||||
NSNumber *key=d[@"id"];id raw=values[key];
|
||||
// Schema 1 documents stored five-feature protection as the legacy bool.
|
||||
// New numeric values win when present; old archives gain its exact 0/1.
|
||||
if(key.unsignedIntValue==PGFeatureProtectionAmount && !raw && [values[@(PGFeatureProtect)] isKindOfClass:NSNumber.class])
|
||||
raw=@([values[@(PGFeatureProtect)] boolValue]?1:0);
|
||||
safe[key]=@(PGControlValue(raw,d));
|
||||
if([d[@"bypass"] boolValue] && [disabled containsObject:key]) [bypassed addObject:key];
|
||||
}
|
||||
state->_values=[safe copy];state->_disabledParameters=[bypassed copy];state->_hasEdits=edited;return state;
|
||||
}
|
||||
- (instancetype)init {if((self=[super init])) {_values=@{};_disabledParameters=[NSSet set];}return self;}
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||
NSInteger version=[coder decodeIntegerForKey:@"schema"];
|
||||
if(version!=1) return nil;
|
||||
NSSet *classes=[NSSet setWithObjects:NSDictionary.class,NSNumber.class,nil];
|
||||
NSDictionary *values=[coder decodeObjectOfClasses:classes forKey:@"values"];
|
||||
NSSet *disabled=[coder decodeObjectOfClasses:[NSSet setWithObjects:NSSet.class,NSNumber.class,nil] forKey:@"disabled"];
|
||||
return [PGInspectorState stateWithValues:values?:@{} disabledParameters:disabled?:[NSSet set] hasEdits:[coder decodeBoolForKey:@"edited"]];
|
||||
}
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeInteger:1 forKey:@"schema"];[coder encodeObject:_values forKey:@"values"];
|
||||
[coder encodeObject:_disabledParameters forKey:@"disabled"];[coder encodeBool:_hasEdits forKey:@"edited"];
|
||||
}
|
||||
- (id)copyWithZone:(NSZone *)zone {return [PGInspectorState stateWithValues:_values disabledParameters:_disabledParameters hasEdits:_hasEdits];}
|
||||
- (double)valueForParameter:(UInt32)identifier {NSDictionary *d=PGInspectorDescriptor(identifier);return d?PGControlValue(_values[@(identifier)],d):0;}
|
||||
- (BOOL)isParameterEnabled:(UInt32)identifier {return ![_disabledParameters containsObject:@(identifier)];}
|
||||
- (BOOL)isEqual:(id)object {
|
||||
if(object==self)return YES;
|
||||
if(![object isKindOfClass:PGInspectorState.class])return NO;
|
||||
PGInspectorState *other=object;return _hasEdits==other.hasEdits && [_values isEqual:other.values] && [_disabledParameters isEqual:other.disabledParameters];
|
||||
}
|
||||
- (NSUInteger)hash {return _values.hash ^ _disabledParameters.hash ^ _hasEdits;}
|
||||
- (NSObject<NSSecureCoding,NSCopying> *)interpolateBetween:(NSObject<NSSecureCoding,NSCopying> *)rightValue withWeight:(float)weight {
|
||||
if(![rightValue isKindOfClass:PGInspectorState.class])return [self copy];
|
||||
PGInspectorState *right=(PGInspectorState *)rightValue;double t=isfinite(weight)?fmin(1,fmax(0,weight)):0;
|
||||
if(t==0)return [self copy];if(t==1)return [right copy];
|
||||
if(!_hasEdits || !right.hasEdits)return [self copy];
|
||||
NSMutableDictionary *values=[NSMutableDictionary new];
|
||||
for(NSDictionary *d in PGInspectorControls()) {
|
||||
UInt32 key=[d[@"id"] unsignedIntValue];double a=[self valueForParameter:key],b=[right valueForParameter:key];
|
||||
// Menus, switches and enable masks hold the preceding key until the
|
||||
// next key. Numeric slider values interpolate within their valid range.
|
||||
values[@(key)]=[d[@"storage"] isEqual:@"float"]?@(a+(b-a)*t):@(a);
|
||||
}
|
||||
return [PGInspectorState stateWithValues:values disabledParameters:_disabledParameters hasEdits:_hasEdits||right.hasEdits];
|
||||
}
|
||||
@end
|
||||
|
||||
PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorState *state) {
|
||||
if(!state.hasEdits)return s;
|
||||
for(NSDictionary *d in PGInspectorControls()) {
|
||||
NSString *storage=d[@"storage"];if([storage isEqual:@"master"])continue;
|
||||
UInt32 key=[d[@"id"] unsignedIntValue];double value=[state valueForParameter:key];
|
||||
if(![state isParameterEnabled:key]) value=0;
|
||||
size_t offset=[d[@"offset"] unsignedIntegerValue];
|
||||
if([storage isEqual:@"float"]) *(float *)((uint8_t *)&s+offset)=value;
|
||||
else *(uint32_t *)((uint8_t *)&s+offset)=(uint32_t)value;
|
||||
}
|
||||
s.featureProtection=s.featureProtectionAmount>0;
|
||||
if(![state valueForParameter:PGSmoothingEnabled])s.amount=s.midtoneDetail=s.highPassDetail=s.microDetail=0;
|
||||
if(![state valueForParameter:PGColorEnabled]) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
|
||||
if(![state valueForParameter:PGShapeEnabled]) {s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;s.showTrackBox=0;}
|
||||
return s;
|
||||
}
|
||||
|
||||
@interface PGInspectorController ()
|
||||
@property (nonatomic,strong,readwrite) PGInspectorState *state;
|
||||
@property (nonatomic,readwrite) BOOL available;
|
||||
@property (nonatomic,copy,readwrite) NSString *statusMessage;
|
||||
@property (nonatomic,strong) id<PROAPIAccessing> manager;
|
||||
@property (nonatomic,strong) id<FxCustomParameterActionAPI_v4> action;
|
||||
@property (nonatomic) BOOL interacting;
|
||||
@property (nonatomic) NSTimeInterval pendingUntil;
|
||||
@property (nonatomic) CMTime pendingTime;
|
||||
@property (nonatomic, strong) id activeSender;
|
||||
@property (nonatomic) BOOL reportedConnection;
|
||||
@property (nonatomic) BOOL reportedMissingAction;
|
||||
@end
|
||||
@implementation PGInspectorController
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
|
||||
if((self=[super init])){_manager=manager;_state=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];_statusMessage=@"";}
|
||||
return self;
|
||||
}
|
||||
- (BOOL)start {
|
||||
if(_interacting)return YES;
|
||||
_action=[_manager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
|
||||
if(!_action){
|
||||
_available=NO;_statusMessage=@"等待宿主连接";
|
||||
if(!_reportedMissingAction){os_log(OS_LOG_DEFAULT,"PGInspector action API unavailable manager=%{public}d viewSender=%{public}d onMain=%{public}d",_manager!=nil,_actionSender!=nil,NSThread.isMainThread);_reportedMissingAction=YES;}
|
||||
return NO;
|
||||
}
|
||||
// A native host can expose parameter APIs only after the custom view's
|
||||
// action has established its document context. Never gate startAction on
|
||||
// those APIs being available before entering the action.
|
||||
BOOL readBefore=NO,writeBefore=NO;
|
||||
if(!_reportedConnection){
|
||||
readBefore=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]!=nil;
|
||||
writeBefore=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]!=nil;
|
||||
}
|
||||
_activeSender=_actionSender ?: self;
|
||||
[_action startAction:_activeSender];_interacting=YES;
|
||||
@try {
|
||||
id retrieval=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
id setting=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
_available=retrieval && setting;
|
||||
if(!_reportedConnection){
|
||||
os_log(OS_LOG_DEFAULT,"PGInspector connection readBefore=%{public}d writeBefore=%{public}d readInAction=%{public}d writeInAction=%{public}d viewSender=%{public}d onMain=%{public}d",readBefore,writeBefore,retrieval!=nil,setting!=nil,[_activeSender isKindOfClass:NSView.class],NSThread.isMainThread);
|
||||
_reportedConnection=YES;
|
||||
}
|
||||
if(!_available){_statusMessage=@"等待宿主连接";[self finish];return NO;}
|
||||
} @catch(NSException *exception) {[self finish];@throw exception;}
|
||||
return YES;
|
||||
}
|
||||
- (void)finish {if(_interacting){_interacting=NO;[_action endAction:_activeSender];_action=nil;_activeSender=nil;}}
|
||||
- (void)readAtTime:(CMTime)time {
|
||||
if(_pendingUntil && CMTimeCompare(time,_pendingTime)!=0)_pendingUntil=0;
|
||||
id<FxParameterRetrievalAPI_v6> api=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
NSObject *object=nil;[api getCustomParameterValue:&object fromParameter:PGInspectorPanel atTime:time];
|
||||
if([object isKindOfClass:PGInspectorState.class] && [(PGInspectorState *)object hasEdits]) {
|
||||
PGInspectorState *host=(PGInspectorState *)object;
|
||||
if([host isEqual:_state])_pendingUntil=0;
|
||||
if(!_pendingUntil || NSDate.timeIntervalSinceReferenceDate>=_pendingUntil)_state=[host copy];
|
||||
return;
|
||||
}
|
||||
if(_pendingUntil && NSDate.timeIntervalSinceReferenceDate<_pendingUntil)return;
|
||||
NSMutableDictionary *values=[NSMutableDictionary new];
|
||||
for(NSDictionary *d in PGInspectorControls()) {
|
||||
UInt32 key=[d[@"id"] unsignedIntValue];double value=[d[@"default"] doubleValue];
|
||||
if([d[@"kind"] isEqual:@"preset"]) {int menu=(int)value;if([api getIntValue:&menu fromParameter:key atTime:time])value=menu;}
|
||||
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) {BOOL on=value!=0;if([api getBoolValue:&on fromParameter:key atTime:time])value=on;}
|
||||
else [api getFloatValue:&value fromParameter:key atTime:time];
|
||||
values[@(key)]=@(value);
|
||||
}
|
||||
// The old bool can still disable protection. A true value must preserve a
|
||||
// numeric setting already read above; its registered default is one for
|
||||
// old documents that have no independently stored numeric parameter.
|
||||
BOOL legacyFeatureProtection=YES;
|
||||
if([api getBoolValue:&legacyFeatureProtection fromParameter:PGFeatureProtect atTime:time] && !legacyFeatureProtection)
|
||||
values[@(PGFeatureProtectionAmount)]=@0;
|
||||
BOOL legacyDetail=YES;[api getBoolValue:&legacyDetail fromParameter:PGDetailEnabled atTime:time];
|
||||
NSMutableSet *disabled=legacyDetail?[NSMutableSet set]:[NSMutableSet setWithObjects:@(PGHighPass),@(PGSharpness),nil];
|
||||
// Earlier versions put high-pass smoothing under a different group. Keep
|
||||
// its effective state when importing a document whose local-skin group was
|
||||
// off, rather than turning a still-active global effect off on first edit.
|
||||
if(![values[@(PGSmoothingEnabled)] boolValue] && legacyDetail && [values[@(PGHighPass)] doubleValue]>0) {
|
||||
values[@(PGSmoothingEnabled)]=@YES;
|
||||
[disabled addObject:@(PGMidtone)];[disabled addObject:@(PGAmount)];[disabled addObject:@(PGMicroDetail)];
|
||||
}
|
||||
_state=[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:NO];
|
||||
}
|
||||
- (void)refresh {
|
||||
if(_interacting)return;
|
||||
if(![self start])return;
|
||||
@try {[self readAtTime:[_action currentTime]];_statusMessage=@"";} @finally {[self finish];}
|
||||
}
|
||||
- (void)documentDidChange {if(_interacting)return;_pendingUntil=0;[self refresh];}
|
||||
- (void)beginInteraction {
|
||||
if(_interacting)return;
|
||||
if([self start]) {
|
||||
// A new gesture starts from the authoritative document, including an
|
||||
// immediate undo. The short pending hold only prevents display flicker;
|
||||
// it must never resurrect an undone value in a subsequent edit.
|
||||
_pendingUntil=0;[self readAtTime:[_action currentTime]];
|
||||
}
|
||||
}
|
||||
- (void)endInteraction {[self finish];}
|
||||
- (BOOL)commit:(PGInspectorState *)next {
|
||||
if([next isEqual:_state] && _state.hasEdits)return YES;
|
||||
id<FxParameterSettingAPI_v5> api=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
CMTime time=[_action currentTime];
|
||||
// The published object is authoritative. Only mirror after its write was
|
||||
// accepted; a rejected write must never leave half-applied hidden values.
|
||||
if(![api setCustomParameterValue:next toParameter:PGInspectorPanel atTime:time]){_statusMessage=@"参数未能保存,请重试";return NO;}
|
||||
for(NSDictionary *d in PGInspectorControls()) {
|
||||
UInt32 key=[d[@"id"] unsignedIntValue];double value=[next valueForParameter:key];
|
||||
if(![next isParameterEnabled:key])value=0;
|
||||
double previous=[_state isParameterEnabled:key]?[_state valueForParameter:key]:0;
|
||||
if(previous==value)continue;
|
||||
if([d[@"kind"] isEqual:@"preset"]) [api setIntValue:(int)value toParameter:key atTime:time];
|
||||
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) [api setBoolValue:value!=0 toParameter:key atTime:time];
|
||||
else [api setFloatValue:value toParameter:key atTime:time];
|
||||
}
|
||||
BOOL featureProtection=[next isParameterEnabled:PGFeatureProtectionAmount] && [next valueForParameter:PGFeatureProtectionAmount]>0;
|
||||
id<FxParameterRetrievalAPI_v6> reader=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
BOOL previousProtection=!featureProtection;
|
||||
if(![reader getBoolValue:&previousProtection fromParameter:PGFeatureProtect atTime:time] || previousProtection!=featureProtection)
|
||||
[api setBoolValue:featureProtection toParameter:PGFeatureProtect atTime:time];
|
||||
_state=next;_pendingUntil=NSDate.timeIntervalSinceReferenceDate+.35;_pendingTime=time;_statusMessage=@"";return YES;
|
||||
}
|
||||
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier {
|
||||
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d || !isfinite(value))return NO;
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {
|
||||
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=@(PGControlValue(@(value),d));
|
||||
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:_state.disabledParameters hasEdits:YES]];
|
||||
} @finally {if(owned)[self finish];}return ok;
|
||||
}
|
||||
- (BOOL)toggleEnabledForParameter:(UInt32)identifier {
|
||||
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
|
||||
if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"]) {
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {ok=[self setValue:![_state valueForParameter:identifier] forParameter:identifier];}
|
||||
@finally {if(owned)[self finish];}return ok;
|
||||
}
|
||||
if(![d[@"bypass"] boolValue])return NO;
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {
|
||||
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];NSNumber *key=@(identifier);
|
||||
if([disabled containsObject:key])[disabled removeObject:key];else [disabled addObject:key];
|
||||
ok=[self commit:[PGInspectorState stateWithValues:_state.values disabledParameters:disabled hasEdits:YES]];
|
||||
} @finally {if(owned)[self finish];}return ok;
|
||||
}
|
||||
- (BOOL)resetParameter:(UInt32)identifier {
|
||||
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {
|
||||
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=d[@"default"];
|
||||
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];[disabled removeObject:@(identifier)];
|
||||
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:YES]];
|
||||
} @finally {if(owned)[self finish];}return ok;
|
||||
}
|
||||
- (void)dealloc {if(_interacting)[_action endAction:_activeSender];}
|
||||
@end
|
||||
@@ -0,0 +1,16 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
#import "PGInspectorState.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Native inspector controls. Host access belongs exclusively to the controller.
|
||||
@interface PGInspectorView : NSView
|
||||
@property (nonatomic, strong, readonly) PGInspectorController *controller;
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
|
||||
/// Also permits a deterministic controller in an offscreen preview or test.
|
||||
- (instancetype)initWithController:(PGInspectorController *)controller;
|
||||
- (void)refreshFromHost;
|
||||
- (void)hostParameterChanged;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,369 @@
|
||||
#import "PGInspectorView.h"
|
||||
#include <math.h>
|
||||
|
||||
static NSColor *PGUIColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a) {
|
||||
return [NSColor colorWithSRGBRed:r green:g blue:b alpha:a];
|
||||
}
|
||||
static NSColor *PGUIText(void) { return PGUIColor(.88,.91,.95,1); }
|
||||
static NSColor *PGUIMuted(void) { return PGUIColor(.60,.65,.73,1); }
|
||||
static NSTextField *PGUILabel(NSString *text, CGFloat size, NSFontWeight weight) {
|
||||
NSTextField *label=[NSTextField labelWithString:text ?: @""];
|
||||
label.font=[NSFont systemFontOfSize:size weight:weight]; label.textColor=PGUIText();
|
||||
label.lineBreakMode=NSLineBreakByTruncatingTail; return label;
|
||||
}
|
||||
static CGFloat PGValueScale(NSDictionary *descriptor) {
|
||||
return [descriptor[@"format"] isEqual:@"percent"] && [descriptor[@"max"] doubleValue]<=10 ? 100 : 1;
|
||||
}
|
||||
static NSString *PGFormattedValue(NSDictionary *descriptor, double value) {
|
||||
NSString *format=descriptor[@"format"];
|
||||
if ([format isEqual:@"percent"]) return [NSString stringWithFormat:@"%.0f%%",value*PGValueScale(descriptor)];
|
||||
if ([format isEqual:@"degrees"]) return [NSString stringWithFormat:@"%+.0f°",value];
|
||||
if ([format isEqual:@"signed"]) return [NSString stringWithFormat:@"%.2f",value];
|
||||
if ([format isEqual:@"integer"]) return [NSString stringWithFormat:@"%.0f",value];
|
||||
return [NSString stringWithFormat:@"%.1f",value];
|
||||
}
|
||||
|
||||
@interface PGInspectorGlyphButton : NSButton
|
||||
@property (nonatomic, copy) NSString *glyph;
|
||||
@property (nonatomic, strong) NSColor *accent;
|
||||
@property (nonatomic, strong) NSColor *swatch;
|
||||
@end
|
||||
@implementation PGInspectorGlyphButton
|
||||
- (BOOL)isFlipped { return YES; }
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
(void)dirtyRect; NSRect b=NSInsetRect(self.bounds,1,1);
|
||||
CGFloat opacity=self.enabled?1:.35;
|
||||
NSColor *accent=[self.accent colorWithAlphaComponent:opacity];
|
||||
BOOL on=self.state==NSControlStateValueOn;
|
||||
if ([self.glyph isEqual:@"swatch"]) {
|
||||
NSRect circle=NSInsetRect(b,3,3);
|
||||
[[(self.swatch ?: PGUIColor(.12,.19,.23,1)) colorWithAlphaComponent:opacity] setFill]; [[NSBezierPath bezierPathWithOvalInRect:circle] fill];
|
||||
[[PGUIText() colorWithAlphaComponent:.25*opacity] setStroke];
|
||||
NSBezierPath *ring=[NSBezierPath bezierPathWithOvalInRect:circle];ring.lineWidth=1;[ring stroke];
|
||||
if (!self.swatch) {
|
||||
[PGUIMuted() setStroke];NSBezierPath *slash=[NSBezierPath bezierPath];
|
||||
[slash moveToPoint:NSMakePoint(NSMinX(circle)+4,NSMinY(circle)+4)];[slash lineToPoint:NSMakePoint(NSMaxX(circle)-4,NSMaxY(circle)-4)];slash.lineWidth=1.5;[slash stroke];
|
||||
}
|
||||
if(on) { [accent setStroke];NSBezierPath *selection=[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(b,.5,.5)];selection.lineWidth=1.5;[selection stroke]; }
|
||||
} else if ([self.glyph isEqual:@"switch"]) {
|
||||
NSRect pill=NSInsetRect(b,1,3);
|
||||
[(on?accent:PGUIColor(.20,.25,.34,opacity)) setFill];[[NSBezierPath bezierPathWithRoundedRect:pill xRadius:NSHeight(pill)/2 yRadius:NSHeight(pill)/2]fill];
|
||||
CGFloat diameter=NSHeight(pill)-4;CGFloat x=on?NSMaxX(pill)-diameter-2:NSMinX(pill)+2;
|
||||
[[NSColor colorWithWhite:.98 alpha:opacity]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(x,NSMinY(pill)+2,diameter,diameter)]fill];
|
||||
} else {
|
||||
NSBezierPath *back=[NSBezierPath bezierPathWithRoundedRect:b xRadius:7 yRadius:7];
|
||||
[[self.accent colorWithAlphaComponent:([self.glyph isEqual:@"eye"]?.055:.13)*opacity]setFill];[back fill];
|
||||
if ([self.glyph isEqual:@"eye"]) {
|
||||
[[self.accent colorWithAlphaComponent:.35*opacity]setStroke];back.lineWidth=.7;[back stroke];
|
||||
NSPoint c=NSMakePoint(NSMidX(b),NSMidY(b));
|
||||
NSBezierPath *eye=[NSBezierPath bezierPath];[eye moveToPoint:NSMakePoint(c.x-6,c.y)];
|
||||
[eye curveToPoint:NSMakePoint(c.x+6,c.y) controlPoint1:NSMakePoint(c.x-3,c.y-5.5) controlPoint2:NSMakePoint(c.x+3,c.y-5.5)];
|
||||
[eye curveToPoint:NSMakePoint(c.x-6,c.y) controlPoint1:NSMakePoint(c.x+3,c.y+5.5) controlPoint2:NSMakePoint(c.x-3,c.y+5.5)];eye.lineWidth=1.3;[accent setStroke];[eye stroke];
|
||||
if(on){[accent setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x-1.8,c.y-1.8,3.6,3.6)]fill];}
|
||||
else{NSBezierPath *slash=[NSBezierPath bezierPath];[slash moveToPoint:NSMakePoint(c.x-6,c.y+6)];[slash lineToPoint:NSMakePoint(c.x+6,c.y-6)];slash.lineWidth=1.2;[slash stroke];}
|
||||
} else {
|
||||
NSString *symbol=[self.glyph isEqual:@"disclosure"]?(on?@"⌄":@"›"):@"↻";
|
||||
NSFont *font=[NSFont systemFontOfSize:[self.glyph isEqual:@"disclosure"]?19:18 weight:NSFontWeightSemibold];
|
||||
NSDictionary *attributes=@{NSFontAttributeName:font,NSForegroundColorAttributeName:[self.accent colorWithAlphaComponent:(on?1:.58)*opacity]};
|
||||
NSSize size=[symbol sizeWithAttributes:attributes];[symbol drawAtPoint:NSMakePoint(NSMidX(b)-size.width/2,NSMidY(b)-size.height/2)withAttributes:attributes];
|
||||
}
|
||||
}
|
||||
if(self.window.firstResponder==self){[[self.accent colorWithAlphaComponent:.8]setStroke];NSBezierPath *focus=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:7 yRadius:7];focus.lineWidth=1;[focus stroke];}
|
||||
}
|
||||
- (void)setState:(NSControlStateValue)state { [super setState:state];self.needsDisplay=YES; }
|
||||
- (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled];self.needsDisplay=YES; }
|
||||
@end
|
||||
static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id target, SEL action) {
|
||||
PGInspectorGlyphButton *button=[[PGInspectorGlyphButton alloc]initWithFrame:NSZeroRect];
|
||||
button.glyph=glyph;button.accent=accent;button.bordered=NO;button.title=@"";
|
||||
[button setButtonType:([glyph isEqual:@"eye"]||[glyph isEqual:@"switch"]||[glyph isEqual:@"disclosure"]||[glyph isEqual:@"swatch"])?NSButtonTypePushOnPushOff:NSButtonTypeMomentaryPushIn];
|
||||
button.target=target;button.action=action;button.focusRingType=NSFocusRingTypeExterior;
|
||||
return button;
|
||||
}
|
||||
|
||||
@interface PGInspectorSliderCell : NSSliderCell
|
||||
@property (nonatomic, strong) NSColor *accent;
|
||||
@end
|
||||
@implementation PGInspectorSliderCell
|
||||
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
|
||||
(void)flipped;NSRect bar=NSMakeRect(NSMinX(rect),NSMidY(rect)-2,NSWidth(rect),4);
|
||||
[PGUIColor(.08,.13,.20,self.enabled?1:.5)setFill];NSBezierPath *track=[NSBezierPath bezierPathWithRoundedRect:bar xRadius:2 yRadius:2];[track fill];
|
||||
[[self.accent colorWithAlphaComponent:.18]setStroke];track.lineWidth=.7;[track stroke];
|
||||
double range=self.maxValue-self.minValue;if(range<=0)return;
|
||||
CGFloat from=self.minValue<0?(CGFloat)((0-self.minValue)/range):0;
|
||||
CGFloat to=(CGFloat)((self.doubleValue-self.minValue)/range);
|
||||
NSRect fill=NSMakeRect(NSMinX(bar)+MIN(from,to)*NSWidth(bar),NSMinY(bar),fabs(to-from)*NSWidth(bar),NSHeight(bar));
|
||||
[[self.accent colorWithAlphaComponent:self.enabled?.85:.23]setFill];[[NSBezierPath bezierPathWithRoundedRect:fill xRadius:2 yRadius:2]fill];
|
||||
}
|
||||
- (void)drawKnob:(NSRect)knobRect {
|
||||
CGFloat d=12;NSRect r=NSMakeRect(NSMidX(knobRect)-d/2,NSMidY(knobRect)-d/2,d,d);
|
||||
[[self.accent colorWithAlphaComponent:self.enabled?.22:.08]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(r,-2,-2)]fill];
|
||||
[[NSColor colorWithWhite:.97 alpha:self.enabled?1:.5]setFill];NSBezierPath *knob=[NSBezierPath bezierPathWithOvalInRect:r];[knob fill];
|
||||
[[self.accent colorWithAlphaComponent:self.enabled?.85:.25]setStroke];knob.lineWidth=1;[knob stroke];
|
||||
}
|
||||
- (CGFloat)knobThickness { return 14; }
|
||||
@end
|
||||
@interface PGInspectorSlider : NSSlider
|
||||
@property (nonatomic, copy) void (^interactionBegan)(void);
|
||||
@property (nonatomic, copy) void (^interactionEnded)(void);
|
||||
@end
|
||||
@implementation PGInspectorSlider
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
if(!self.enabled)return;if(self.interactionBegan)self.interactionBegan();
|
||||
@try{[super mouseDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
|
||||
}
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
if(self.interactionBegan)self.interactionBegan();
|
||||
@try{[super keyDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
|
||||
}
|
||||
@end
|
||||
|
||||
@interface PGInspectorNumberCell : NSTextFieldCell
|
||||
@end
|
||||
@implementation PGInspectorNumberCell
|
||||
- (NSRect)drawingRectForBounds:(NSRect)bounds {
|
||||
NSRect rect=[super drawingRectForBounds:bounds];CGFloat height=[self cellSizeForBounds:bounds].height;
|
||||
if(NSHeight(rect)>height){rect.origin.y+=(NSHeight(rect)-height)*.5;rect.size.height=height;}return rect;
|
||||
}
|
||||
@end
|
||||
@interface PGInspectorNumberField : NSTextField
|
||||
@end
|
||||
@implementation PGInspectorNumberField
|
||||
+ (Class)cellClass { return PGInspectorNumberCell.class; }
|
||||
@end
|
||||
|
||||
@class PGInspectorRow;
|
||||
@interface PGInspectorCard : NSView
|
||||
@property (nonatomic, strong) NSColor *accent;
|
||||
@property (nonatomic, copy) NSString *heading;
|
||||
@property (nonatomic, strong) NSTextField *titleLabel;
|
||||
@property (nonatomic, strong) PGInspectorGlyphButton *master;
|
||||
@property (nonatomic, copy) NSArray<PGInspectorRow *> *rows;
|
||||
@property (nonatomic) BOOL expanded;
|
||||
- (CGFloat)heightForWidth:(CGFloat)width;
|
||||
@end
|
||||
@interface PGInspectorView ()
|
||||
@property (nonatomic, strong, readwrite) PGInspectorController *controller;
|
||||
- (void)reloadControls;
|
||||
- (void)beginControlInteraction;
|
||||
- (void)endControlInteraction;
|
||||
- (void)writeValue:(double)value parameter:(UInt32)identifier;
|
||||
@end
|
||||
@interface PGInspectorRow : NSView <NSTextFieldDelegate>
|
||||
@property (nonatomic, weak) PGInspectorView *inspector;
|
||||
@property (nonatomic, copy) NSDictionary *descriptor;
|
||||
@property (nonatomic, strong) NSColor *accent;
|
||||
@property (nonatomic, strong) NSTextField *label;
|
||||
@property (nonatomic, strong) NSTextField *valueField;
|
||||
@property (nonatomic) BOOL numberHasUnsubmittedEdit;
|
||||
@property (nonatomic, strong) NSTextField *hint;
|
||||
@property (nonatomic, strong) PGInspectorSlider *slider;
|
||||
@property (nonatomic, strong) PGInspectorGlyphButton *eye;
|
||||
@property (nonatomic, strong) PGInspectorGlyphButton *reset;
|
||||
@property (nonatomic, strong) PGInspectorGlyphButton *toggle;
|
||||
@property (nonatomic, copy) NSArray<PGInspectorGlyphButton *> *swatches;
|
||||
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector;
|
||||
- (CGFloat)heightForWidth:(CGFloat)width;
|
||||
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available;
|
||||
@end
|
||||
@implementation PGInspectorRow
|
||||
- (BOOL)isFlipped { return YES; }
|
||||
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector {
|
||||
if((self=[super initWithFrame:NSZeroRect])) {
|
||||
_descriptor=[descriptor copy];_accent=accent;_inspector=inspector;
|
||||
self.identifier=[NSString stringWithFormat:@"pg.parameter.%@",descriptor[@"id"]];
|
||||
NSString *name=descriptor[@"name"];_label=PGUILabel(name,12,NSFontWeightSemibold);[self addSubview:_label];
|
||||
NSString *kind=descriptor[@"kind"];
|
||||
if([descriptor[@"bypass"]boolValue]){
|
||||
_eye=PGUIButton(@"eye",accent,self,@selector(toggleEnabled:));_eye.toolTip=[@"启用 / 停用 · " stringByAppendingString:name];_eye.accessibilityLabel=_eye.toolTip;[self addSubview:_eye];
|
||||
}
|
||||
if([kind isEqual:@"preset"]){
|
||||
NSArray *colors=@[NSNull.null,PGUIColor(.95,.87,.80,1),PGUIColor(.88,.88,.95,1),PGUIColor(1,.86,.72,1),PGUIColor(.99,.78,.75,1),PGUIColor(.76,.57,.39,1),PGUIColor(.48,.32,.22,1)];
|
||||
NSArray *options=descriptor[@"options"] ?: @[];NSMutableArray *buttons=[NSMutableArray new];
|
||||
for(NSUInteger i=0;i<7;i++){
|
||||
PGInspectorGlyphButton *button=PGUIButton(@"swatch",accent,self,@selector(selectPreset:));button.tag=i;button.swatch=i?colors[i]:nil;
|
||||
button.toolTip=i<options.count?options[i]:[NSString stringWithFormat:@"%@ %lu",name,(unsigned long)i];button.accessibilityLabel=button.toolTip;
|
||||
[self addSubview:button];[buttons addObject:button];
|
||||
}_swatches=[buttons copy];
|
||||
}else{
|
||||
_reset=PGUIButton(@"reset",accent,self,@selector(resetValue:));_reset.toolTip=[@"重置 · "stringByAppendingString:name];_reset.accessibilityLabel=_reset.toolTip;[self addSubview:_reset];
|
||||
if([kind isEqual:@"toggle"]){
|
||||
_toggle=PGUIButton(@"switch",accent,self,@selector(toggleValue:));_toggle.accessibilityLabel=name;[self addSubview:_toggle];
|
||||
_hint=PGUILabel(@"",10,NSFontWeightMedium);_hint.textColor=PGUIMuted();_hint.alignment=NSTextAlignmentRight;[self addSubview:_hint];
|
||||
_label.lineBreakMode=NSLineBreakByWordWrapping;_label.maximumNumberOfLines=2;
|
||||
}else{
|
||||
_slider=[[PGInspectorSlider alloc]initWithFrame:NSZeroRect];PGInspectorSliderCell *cell=[PGInspectorSliderCell new];cell.accent=accent;_slider.cell=cell;
|
||||
_slider.minValue=[descriptor[@"min"]doubleValue];_slider.maxValue=[descriptor[@"max"]doubleValue];_slider.continuous=YES;_slider.target=self;_slider.action=@selector(sliderChanged:);_slider.accessibilityLabel=name;
|
||||
__weak PGInspectorView *weakInspector=inspector;
|
||||
_slider.interactionBegan=^{[weakInspector beginControlInteraction];};_slider.interactionEnded=^{[weakInspector endControlInteraction];};[self addSubview:_slider];
|
||||
_valueField=[[PGInspectorNumberField alloc]initWithFrame:NSZeroRect];_valueField.font=[NSFont monospacedDigitSystemFontOfSize:11 weight:NSFontWeightSemibold];_valueField.textColor=PGUIText();_valueField.alignment=NSTextAlignmentCenter;
|
||||
_valueField.bordered=NO;_valueField.bezeled=NO;_valueField.drawsBackground=NO;_valueField.backgroundColor=PGUIColor(.14,.19,.26,1);_valueField.wantsLayer=YES;_valueField.layer.backgroundColor=_valueField.backgroundColor.CGColor;_valueField.layer.cornerRadius=7;_valueField.layer.borderWidth=.7;_valueField.layer.borderColor=[accent colorWithAlphaComponent:.24].CGColor;
|
||||
_valueField.target=self;_valueField.action=@selector(numberCommitted:);_valueField.delegate=self;_valueField.accessibilityLabel=[name stringByAppendingString:@"数值"];[self addSubview:_valueField];
|
||||
}
|
||||
}
|
||||
NSString *key=[descriptor[@"id"]stringValue];
|
||||
NSString *tooltip=descriptor[@"tooltip"];
|
||||
if(tooltip.length){self.toolTip=tooltip;_label.toolTip=tooltip;_toggle.toolTip=tooltip;_slider.toolTip=tooltip;_valueField.toolTip=tooltip;}
|
||||
_slider.identifier=[@"pg.value."stringByAppendingString:key];_valueField.identifier=[@"pg.number."stringByAppendingString:key];
|
||||
_toggle.identifier=[@"pg.toggle."stringByAppendingString:key];_eye.identifier=[@"pg.enable."stringByAppendingString:key];_reset.identifier=[@"pg.reset."stringByAppendingString:key];
|
||||
}return self;
|
||||
}
|
||||
- (CGFloat)heightForWidth:(CGFloat)width {
|
||||
if(_swatches)return 46;
|
||||
if(_toggle)return width<310 && [_descriptor[@"name"]length]>12 ? 48 : 36;
|
||||
return width<310?52:36;
|
||||
}
|
||||
- (void)layout {
|
||||
[super layout];CGFloat w=NSWidth(self.bounds),h=NSHeight(self.bounds);BOOL narrow=w<310;
|
||||
CGFloat labelX=_eye?25:0;
|
||||
_eye.frame=NSMakeRect(0,6,20,22);
|
||||
if(_swatches){
|
||||
_label.frame=NSMakeRect(labelX,14,33,20);CGFloat x=labelX+37;CGFloat d=MIN(28,MAX(12,(w-x-24)/7));CGFloat gap=MIN(8,MAX(0,(w-x-7*d)/6));
|
||||
for(NSUInteger i=0;i<_swatches.count;i++)_swatches[i].frame=NSMakeRect(x+i*(d+gap),7,d,d);return;
|
||||
}
|
||||
_reset.frame=NSMakeRect(w-23,6,23,23);
|
||||
if(_toggle){
|
||||
CGFloat switchX=w-76;_toggle.frame=NSMakeRect(switchX,4+(h-36)/2,46,28);_hint.frame=NSMakeRect(switchX-39,10+(h-36)/2,34,18);
|
||||
_label.frame=NSMakeRect(labelX,h>36?3:9,MAX(20,switchX-labelX-42),h>36?42:22);return;
|
||||
}
|
||||
CGFloat fieldW=49,fieldX=w-23-7-fieldW;_valueField.frame=NSMakeRect(fieldX,5,fieldW,25);
|
||||
if(narrow){_label.frame=NSMakeRect(labelX,8,MAX(20,fieldX-labelX-6),22);_slider.frame=NSMakeRect(labelX,29,MAX(20,w-labelX-30),18);}
|
||||
else{CGFloat labelW=MIN(116,MAX(71,w*.29));_label.frame=NSMakeRect(labelX,9,labelW,21);CGFloat start=labelX+labelW+5;_slider.frame=NSMakeRect(start,7,MAX(20,fieldX-start-8),22);}
|
||||
}
|
||||
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available {
|
||||
UInt32 identifier=[_descriptor[@"id"]unsignedIntValue];double value=[state valueForParameter:identifier];BOOL enabled=[state isParameterEnabled:identifier];
|
||||
_label.alphaValue=master&&enabled?1:.5;_eye.state=enabled?NSControlStateValueOn:NSControlStateValueOff;_eye.enabled=available&&master;
|
||||
_reset.enabled=available;_reset.state=fabs(value-[_descriptor[@"default"]doubleValue])>.000001?NSControlStateValueOn:NSControlStateValueOff;
|
||||
_slider.enabled=available&&master&&enabled;_slider.doubleValue=value;
|
||||
_valueField.enabled=available&&master&&enabled;if(!_valueField.currentEditor || !_numberHasUnsubmittedEdit)_valueField.stringValue=PGFormattedValue(_descriptor,value);
|
||||
_toggle.enabled=available&&master&&enabled;_toggle.state=value>.5?NSControlStateValueOn:NSControlStateValueOff;
|
||||
UInt32 identifierForHint=[_descriptor[@"id"]unsignedIntValue];
|
||||
_hint.stringValue=identifierForHint==PGFastMulti?(value>.5?@"多人":@"单人"):identifierForHint==PGTrackBoxes?(value>.5?@"预览中":@"隐藏"):(value>.5?@"开启":@"关闭");
|
||||
for(NSUInteger i=0;i<_swatches.count;i++){_swatches[i].enabled=available&&master&&enabled;_swatches[i].state=i==(NSUInteger)llround(value)?NSControlStateValueOn:NSControlStateValueOff;}
|
||||
}
|
||||
- (void)sliderChanged:(NSSlider *)sender { [_inspector writeValue:sender.doubleValue parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||
- (void)toggleValue:(NSButton *)sender { [_inspector writeValue:sender.state==NSControlStateValueOn?1:0 parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||
- (void)selectPreset:(NSButton *)sender { [_inspector writeValue:sender.tag parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||
- (void)toggleEnabled:(id)sender { (void)sender;[_inspector.controller toggleEnabledForParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
|
||||
- (void)resetValue:(id)sender { (void)sender;[_inspector.controller resetParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
|
||||
- (void)numberCommitted:(id)sender {
|
||||
// The explicit Return action and the subsequent end-editing notification
|
||||
// describe the same submission. Clear the dirty flag before host callbacks
|
||||
// so focus loss cannot write stale text over a reset or a host undo.
|
||||
_numberHasUnsubmittedEdit=NO;
|
||||
NSString *text=[_valueField.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
if([text hasSuffix:@"%"]||[text hasSuffix:@"°"])text=[text substringToIndex:text.length-1];
|
||||
NSNumber *number=nil;
|
||||
for(NSLocale *locale in @[NSLocale.currentLocale,[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]){
|
||||
NSScanner *scanner=[NSScanner scannerWithString:text];scanner.locale=locale;double parsed;
|
||||
if([scanner scanDouble:&parsed]&&scanner.isAtEnd){number=@(parsed);break;}
|
||||
}
|
||||
if(number&&isfinite(number.doubleValue)){
|
||||
double value=number.doubleValue/PGValueScale(_descriptor);value=fmin([_descriptor[@"max"]doubleValue],fmax([_descriptor[@"min"]doubleValue],value));
|
||||
// Commit explicit input even when it equals our last displayed value:
|
||||
// the document may have changed while the field editor held focus.
|
||||
// The controller rereads the host and deduplicates the actual value.
|
||||
[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]];
|
||||
}else NSBeep();
|
||||
_valueField.stringValue=PGFormattedValue(_descriptor,[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]]);
|
||||
if(sender==_valueField) {
|
||||
[_valueField.window makeFirstResponder:nil];
|
||||
// A remote inspector can retain its field editor after the host has
|
||||
// consumed Return. End the already-submitted edit at the control too.
|
||||
[_valueField abortEditing];
|
||||
}
|
||||
}
|
||||
- (void)controlTextDidChange:(NSNotification *)notification { (void)notification;_numberHasUnsubmittedEdit=YES; }
|
||||
- (void)controlTextDidEndEditing:(NSNotification *)notification {
|
||||
(void)notification;
|
||||
if(_numberHasUnsubmittedEdit)[self numberCommitted:nil];
|
||||
[_inspector refreshFromHost];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGInspectorCard
|
||||
- (BOOL)isFlipped { return YES; }
|
||||
- (CGFloat)heightForWidth:(CGFloat)width {
|
||||
if(!_expanded)return 44;CGFloat height=49;for(PGInspectorRow *row in _rows){height+=[row heightForWidth:MAX(0,width-24)];if([row.descriptor[@"id"]unsignedIntValue]==PGFastMulti)height+=8;}return height+7;
|
||||
}
|
||||
- (void)layout {
|
||||
[super layout];CGFloat w=NSWidth(self.bounds);_titleLabel.frame=NSMakeRect(25,13,MAX(70,w-80),22);_master.frame=NSMakeRect(w-39,10,25,25);
|
||||
CGFloat y=49;for(PGInspectorRow *row in _rows){row.hidden=!_expanded;if(!_expanded)continue;CGFloat h=[row heightForWidth:w-24];row.frame=NSMakeRect(12,y,w-24,h);y+=h;if([row.descriptor[@"id"]unsignedIntValue]==PGFastMulti)y+=8;}
|
||||
}
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
(void)dirtyRect;NSBezierPath *shape=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:13 yRadius:13];
|
||||
[NSGraphicsContext saveGraphicsState];[shape addClip];NSGradient *gradient=[[NSGradient alloc]initWithStartingColor:[_accent blendedColorWithFraction:.87 ofColor:PGUIColor(.09,.14,.21,1)] endingColor:PGUIColor(.055,.085,.14,1)];[gradient drawInRect:self.bounds angle:90];[NSGraphicsContext restoreGraphicsState];
|
||||
[[_accent colorWithAlphaComponent:.22]setStroke];shape.lineWidth=.8;[shape stroke];
|
||||
NSRect pill=NSMakeRect(12,10,MAX(53,[_heading length]*13+20),25);[[_accent colorWithAlphaComponent:.10]setFill];NSBezierPath *p=[NSBezierPath bezierPathWithRoundedRect:pill xRadius:8 yRadius:8];[p fill];[[_accent colorWithAlphaComponent:.32]setStroke];p.lineWidth=.7;[p stroke];
|
||||
if(_expanded){[[PGUIMuted() colorWithAlphaComponent:.20]setStroke];NSBezierPath *line=[NSBezierPath bezierPath];[line moveToPoint:NSMakePoint(12,43)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,43)];line.lineWidth=.5;[line stroke];
|
||||
for(PGInspectorRow *row in _rows)if([row.descriptor[@"id"]unsignedIntValue]==PGFastMulti){CGFloat y=NSMaxY(row.frame)+4;[line removeAllPoints];[line moveToPoint:NSMakePoint(12,y)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,y)];[line stroke];}
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGInspectorView {
|
||||
NSArray<PGInspectorCard *> *_cards;
|
||||
PGInspectorCard *_more;
|
||||
NSTextField *_brand,*_website,*_status;
|
||||
NSTimer *_refreshTimer;
|
||||
NSUInteger _interactionDepth;
|
||||
}
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager { return [self initWithController:[[PGInspectorController alloc]initWithAPIManager:manager]]; }
|
||||
- (instancetype)initWithController:(PGInspectorController *)controller {
|
||||
if((self=[super initWithFrame:NSMakeRect(0,0,320,100)])){
|
||||
_controller=controller;_controller.actionSender=self;self.appearance=[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
|
||||
NSArray *sections=@[@"smooth",@"color",@"shape",@"more"],*headings=@[@"磨皮",@"美白",@"瘦脸",@"更多设置"];
|
||||
NSArray *colors=@[PGUIColor(.28,.83,.96,1),PGUIColor(.34,.89,.84,1),PGUIColor(.59,.69,.98,1),PGUIColor(.54,.64,.76,1)];
|
||||
NSMutableArray *cards=[NSMutableArray new];
|
||||
for(NSUInteger index=0;index<sections.count;index++){
|
||||
NSString *section=sections[index];PGInspectorCard *card=[[PGInspectorCard alloc]initWithFrame:NSZeroRect];card.accent=colors[index];card.heading=headings[index];card.expanded=index<3;
|
||||
card.titleLabel=PGUILabel(headings[index],12,NSFontWeightSemibold);card.titleLabel.textColor=colors[index];[card addSubview:card.titleLabel];NSMutableArray *rows=[NSMutableArray new];
|
||||
for(NSDictionary *descriptor in PGInspectorControls()){
|
||||
if(![descriptor[@"section"]isEqual:section])continue;
|
||||
if([descriptor[@"kind"]isEqual:@"master"]){card.master=PGUIButton(@"eye",colors[index],self,@selector(masterChanged:));card.master.tag=[descriptor[@"id"]integerValue];card.master.identifier=[NSString stringWithFormat:@"pg.master.%@",descriptor[@"id"]];card.master.accessibilityLabel=descriptor[@"name"];card.master.toolTip=descriptor[@"name"];[card addSubview:card.master];continue;}
|
||||
PGInspectorRow *row=[[PGInspectorRow alloc]initWithDescriptor:descriptor accent:colors[index] inspector:self];[rows addObject:row];[card addSubview:row];
|
||||
}
|
||||
card.rows=rows;if(index==3){_more=card;card.master=PGUIButton(@"disclosure",colors[index],self,@selector(moreChanged:));card.master.identifier=@"pg.more";card.master.accessibilityLabel=@"展开更多设置";[card addSubview:card.master];}
|
||||
[self addSubview:card];[cards addObject:card];
|
||||
}_cards=[cards copy];
|
||||
_brand=PGUILabel(@"大鱼 · 人像美化",15,NSFontWeightSemibold);_brand.alignment=NSTextAlignmentCenter;[self addSubview:_brand];
|
||||
_website=PGUILabel(@"dayu.media",10,NSFontWeightMedium);_website.alignment=NSTextAlignmentCenter;_website.textColor=PGUIMuted();[self addSubview:_website];
|
||||
_status=PGUILabel(@"",10,NSFontWeightRegular);_status.textColor=PGUIMuted();_status.alignment=NSTextAlignmentCenter;[self addSubview:_status];
|
||||
[self reloadControls];[self setFrameSize:NSMakeSize(320,[self heightForWidth:320])];
|
||||
}return self;
|
||||
}
|
||||
- (BOOL)isFlipped { return YES; }
|
||||
- (CGFloat)heightForWidth:(CGFloat)width { CGFloat h=12;for(PGInspectorCard *card in _cards)h+=[card heightForWidth:MAX(0,width-20)]+10;return h+83; }
|
||||
- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric,[self heightForWidth:NSWidth(self.bounds)>0?NSWidth(self.bounds):320]); }
|
||||
- (NSSize)fittingSize { CGFloat w=NSWidth(self.bounds)>0?NSWidth(self.bounds):320;return NSMakeSize(w,[self heightForWidth:w]); }
|
||||
- (void)setFrameSize:(NSSize)newSize { if(_cards)newSize.height=[self heightForWidth:newSize.width];[super setFrameSize:newSize];[self invalidateIntrinsicContentSize];self.needsLayout=YES; }
|
||||
- (void)layout {
|
||||
[super layout];CGFloat w=NSWidth(self.bounds),y=12;for(PGInspectorCard *card in _cards){CGFloat h=[card heightForWidth:w-20];card.frame=NSMakeRect(10,y,MAX(0,w-20),h);y+=h+10;}
|
||||
_brand.frame=NSMakeRect(12,y+12,MAX(0,w-24),23);_website.frame=NSMakeRect(12,y+38,MAX(0,w-24),17);_status.frame=NSMakeRect(12,y+61,MAX(0,w-24),16);
|
||||
}
|
||||
- (void)drawRect:(NSRect)dirtyRect { [PGUIColor(.035,.060,.105,1)setFill];NSRectFill(dirtyRect); }
|
||||
- (void)reloadControls {
|
||||
PGInspectorState *state=_controller.state;BOOL available=_controller.available;
|
||||
for(PGInspectorCard *card in _cards){BOOL master=card==_more || [state valueForParameter:(UInt32)card.master.tag]>.5;
|
||||
if(card!=_more){card.master.state=master?NSControlStateValueOn:NSControlStateValueOff;card.master.enabled=available;}
|
||||
for(PGInspectorRow *row in card.rows)[row reloadState:state master:master available:available];
|
||||
}
|
||||
_status.stringValue=_controller.statusMessage.length?_controller.statusMessage:@"自动跟随 · 无需手动分析";_status.toolTip=_status.stringValue;
|
||||
}
|
||||
- (BOOL)hasActiveTextEditor { for(PGInspectorCard *card in _cards)for(PGInspectorRow *row in card.rows)if(row.valueField.currentEditor && row.numberHasUnsubmittedEdit)return YES;return NO; }
|
||||
- (void)refreshFromHost { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller refresh];[self reloadControls]; }
|
||||
- (void)hostParameterChanged { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller documentDidChange];[self reloadControls]; }
|
||||
- (void)writeValue:(double)value parameter:(UInt32)identifier { [_controller setValue:value forParameter:identifier];[self reloadControls]; }
|
||||
// Keep refreshes out of the tracking loop, but let writeValue own a complete
|
||||
// FxPlug action for each continuous step. Holding the action until mouse-up
|
||||
// prevents hosts that publish at endAction from previewing intermediate values.
|
||||
- (void)beginControlInteraction { _interactionDepth++; }
|
||||
- (void)endControlInteraction { if(!_interactionDepth)return;if(--_interactionDepth==0)[self refreshFromHost]; }
|
||||
- (void)masterChanged:(NSButton *)sender { [_controller setValue:sender.state==NSControlStateValueOn?1:0 forParameter:(UInt32)sender.tag];[self reloadControls]; }
|
||||
- (void)moreChanged:(NSButton *)sender { _more.expanded=sender.state==NSControlStateValueOn;sender.accessibilityLabel=_more.expanded?@"收起更多设置":@"展开更多设置";[_more setNeedsDisplay:YES];[self setFrameSize:self.frame.size]; }
|
||||
- (void)viewDidMoveToWindow {
|
||||
[super viewDidMoveToWindow];[_refreshTimer invalidate];_refreshTimer=nil;
|
||||
if(self.window){[self refreshFromHost];__weak PGInspectorView *weakSelf=self;_refreshTimer=[NSTimer timerWithTimeInterval:.65 repeats:YES block:^(NSTimer *timer){PGInspectorView *view=weakSelf;if(!view||!view.window){[timer invalidate];return;}[view refreshFromHost];}];[[NSRunLoop mainRunLoop]addTimer:_refreshTimer forMode:NSRunLoopCommonModes];}
|
||||
}
|
||||
- (void)dealloc { [_refreshTimer invalidate]; }
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
// Existing brightness sample ID 1 is migrated to the portrait amount control.
|
||||
enum {
|
||||
PGAmount=1, PGBrightness=2, PGRadius=3, PGSharpness=4, PGMix=5,
|
||||
PGThreshold=7, PGSoftness=8, PGEdgeProtect=9, PGShowMask=10,
|
||||
PGMidtone=12, PGHue=14, PGSaturation=15, PGHighPass=17, PGThin=18,
|
||||
PGFaceBoxes=19, PGSingleFace=20, PGSelection=21, PGShort=22,
|
||||
PGNarrow=23, PGChin=24, PGVFace=25, PGJaw=26, PGCheekbone=27,
|
||||
PGSmallHead=28, PGSmallHeadRange=29, PGFeatureProtect=30, PGTeeth=31,
|
||||
PGEyeBright=32, PGBigEye=33, PGTrackBoxes=35, PGFastMulti=39,
|
||||
PGJumpGuard=40, PGEvenTone=41, PGTonePreset=42, PGInvertMask=43,
|
||||
PGDebugChannel=44, PGTrackEnabled=45, PGTarget=46,
|
||||
PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53,
|
||||
PGTrackData1=60, PGTrackData2=61, PGTrackData3=62,
|
||||
PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72,
|
||||
PGDetailEnabled=73, PGHelp=74,
|
||||
PGFeatureProtectionAmount=75, PGNonSkinProtection=76, PGMicroDetail=77,
|
||||
PGInspectorPanel=210
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
"FCP-BeautyGirl::FxPlug Name" = "大鱼人像美化";
|
||||
@@ -0,0 +1 @@
|
||||
"FCP-BeautyGirl::FxPlug Description" = "Portrait smoothing, skin tone, face shaping and automatic face tracking.";
|
||||
@@ -0,0 +1,6 @@
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
|
||||
int main(int argc, const char *argv[])
|
||||
{
|
||||
[FxPrincipal startServicePrincipal];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// AppDelegate.h
|
||||
// FCP-BeautyGirl
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#import "AppDelegate.h"
|
||||
#import <CoreServices/CoreServices.h>
|
||||
|
||||
@interface AppDelegate ()
|
||||
@property (weak) IBOutlet NSWindow *window;
|
||||
@property (strong) NSTimer *exitTimer;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
self.window.title = @"FCP-BeautyGirl · 大鱼人像美化";
|
||||
[self.window setContentSize:NSMakeSize(800, 450)];
|
||||
self.window.styleMask &= ~NSWindowStyleMaskResizable;
|
||||
self.window.backgroundColor = [NSColor colorWithRed:0.94 green:0.92 blue:0.85 alpha:1];
|
||||
NSImageView *card = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 800, 450)];
|
||||
card.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
card.imageScaling = NSImageScaleProportionallyUpOrDown;
|
||||
NSURL *artwork = [NSBundle.mainBundle URLForResource:@"LauncherInfo" withExtension:@"png"];
|
||||
card.image = [[NSImage alloc] initWithContentsOfURL:artwork];
|
||||
card.accessibilityLabel = @"大鱼人像美化,免费开源,自动注册,无需激活码。主程序启动三秒后自动退出;Final Cut Pro 和 Motion 中的滤镜继续独立运行。";
|
||||
self.window.contentView = card;
|
||||
if (!card.image) {
|
||||
NSTextField *fallback = [NSTextField wrappingLabelWithString:card.accessibilityLabel];
|
||||
fallback.frame = NSMakeRect(48, 160, 704, 130);
|
||||
fallback.font = [NSFont systemFontOfSize:24 weight:NSFontWeightSemibold];
|
||||
[card addSubview:fallback];
|
||||
}
|
||||
[self.window center];
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
// Only this launcher exits. The FxPlug service has its own process/lifetime.
|
||||
self.exitTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer *timer) {
|
||||
[NSApp terminate:nil];
|
||||
}];
|
||||
// Re-register our own bundle. There is no paid activation or account step.
|
||||
NSURL *bundleURL = NSBundle.mainBundle.bundleURL;
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
OSStatus status = LSRegisterURL((__bridge CFURLRef)bundleURL, true);
|
||||
if (status != noErr) NSLog(@"LaunchServices registration status: %d", (int)status);
|
||||
});
|
||||
}
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,693 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate">
|
||||
<connections>
|
||||
<outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="FCP-BeautyGirl" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="FCP-BeautyGirl" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About FCP-BeautyGirl" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide FCP-BeautyGirl" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit FCP-BeautyGirl" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="dMs-cI-mzQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||
<items>
|
||||
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
|
||||
<connections>
|
||||
<action selector="newDocument:" target="-1" id="4Si-XN-c54"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
|
||||
<connections>
|
||||
<action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open Recent" id="tXI-mr-wws">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
|
||||
<items>
|
||||
<menuItem title="Clear Menu" id="vNY-rz-j42">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||
<connections>
|
||||
<action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
|
||||
<connections>
|
||||
<action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
|
||||
<connections>
|
||||
<action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
|
||||
<connections>
|
||||
<action selector="print:" target="-1" id="qaZ-4w-aoO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Format" id="jxT-CU-nIS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
|
||||
<items>
|
||||
<menuItem title="Font" id="Gi5-1S-RQB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
|
||||
<items>
|
||||
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
|
||||
<connections>
|
||||
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
|
||||
<connections>
|
||||
<action selector="underline:" target="-1" id="FYS-2b-JAY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
|
||||
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
|
||||
<menuItem title="Kern" id="jBQ-r6-VK2">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="GUa-eO-cwY">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardKerning:" target="-1" id="6dk-9l-Ckg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="cDB-IK-hbR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffKerning:" target="-1" id="U8a-gz-Maa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Tighten" id="46P-cB-AYj">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="tightenKerning:" target="-1" id="hr7-Nz-8ro"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Loosen" id="ogc-rX-tC1">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="loosenKerning:" target="-1" id="8i4-f9-FKE"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Ligatures" id="o6e-r0-MWq">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="agt-UL-0e3">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardLigatures:" target="-1" id="7uR-wd-Dx6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="J7y-lM-qPV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffLigatures:" target="-1" id="iX2-gA-Ilz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use All" id="xQD-1f-W4t">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useAllLigatures:" target="-1" id="KcB-kA-TuK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Baseline" id="OaQ-X3-Vso">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="3Om-Ey-2VK">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unscript:" target="-1" id="0vZ-95-Ywn"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Superscript" id="Rqc-34-cIF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="superscript:" target="-1" id="3qV-fo-wpU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Subscript" id="I0S-gh-46l">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="subscript:" target="-1" id="Q6W-4W-IGz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Raise" id="2h7-ER-AoG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="raiseBaseline:" target="-1" id="4sk-31-7Q9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Lower" id="1tx-W0-xDw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowerBaseline:" target="-1" id="OF1-bc-KW4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
|
||||
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
|
||||
<connections>
|
||||
<action selector="orderFrontColorPanel:" target="-1" id="mSX-Xz-DV3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
|
||||
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyFont:" target="-1" id="GJO-xA-L4q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteFont:" target="-1" id="JfD-CL-leO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Text" id="Fal-I4-PZk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Text" id="d9c-me-L2H">
|
||||
<items>
|
||||
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
|
||||
<connections>
|
||||
<action selector="alignLeft:" target="-1" id="zUv-R1-uAa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
|
||||
<connections>
|
||||
<action selector="alignCenter:" target="-1" id="spX-mk-kcS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Justify" id="J5U-5w-g23">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="alignJustified:" target="-1" id="ljL-7U-jND"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
|
||||
<connections>
|
||||
<action selector="alignRight:" target="-1" id="r48-bG-YeY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
|
||||
<menuItem title="Writing Direction" id="H1b-Si-o9J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
|
||||
<items>
|
||||
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="YGs-j5-SAR">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="qtV-5e-UBP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="Lbh-J2-qVU">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="S0X-9S-QSf"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="jFq-tB-4Kx">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="5fk-qB-AqJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
|
||||
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="Nop-cj-93Q">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionNatural:" target="-1" id="lPI-Se-ZHp"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="BgM-ve-c93">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="caW-Bv-w94"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="RB4-Sm-HuC">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="EXD-6r-ZUu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
|
||||
<menuItem title="Show Ruler" id="vLm-3I-IUL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleRuler:" target="-1" id="FOx-HJ-KwY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyRuler:" target="-1" id="71i-fW-3W2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteRuler:" target="-1" id="cSh-wd-qM2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="runToolbarCustomizationPalette:" target="-1" id="pQI-g3-MTW"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleSourceList:" target="-1" id="iwa-gc-5KM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="FCP-BeautyGirl Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<window title="FCP-BeautyGirl" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="480" height="360"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="360"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FCP-BeautyGirl · 大鱼人像美化</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>17</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string></string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.video</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 2.9 MiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1228
|
||||
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\vieww9600\viewh8400\viewkind0
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
|
||||
Text here
|
||||
\f0\fs24 \cf0 \
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
int main(int argc, const char * argv[])
|
||||
{
|
||||
return NSApplicationMain(argc, argv);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户