diff --git a/Templates/大鱼人像美化/PARAMETERS.json b/Templates/大鱼人像美化/PARAMETERS.json index e83652a..df2c01f 100644 --- a/Templates/大鱼人像美化/PARAMETERS.json +++ b/Templates/大鱼人像美化/PARAMETERS.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.0.2", "pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "filterObject": "510000005", "visiblePublished": 46, diff --git a/Templates/大鱼人像美化/README.md b/Templates/大鱼人像美化/README.md index e49bff8..ba2c07e 100644 --- a/Templates/大鱼人像美化/README.md +++ b/Templates/大鱼人像美化/README.md @@ -1,6 +1,6 @@ # 大鱼人像美化 FCP 效果模板 -版本:1.0.1。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。 +版本:1.0.2。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。 ## 默认数值 @@ -28,6 +28,6 @@ Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有 前版生成稿(SHA256 `3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478`)将效果源的 Source Media 设为0,并删除了原生clip/footage。用户在FCP应用后实测整幅黑屏。这是模板装配错误;保留Type=3本身并不能证明FCP输入替换链完整。此前XML解析、发布路径和无缺媒体检查通过,也没有覆盖真实FCP画面输入,不能当作该模板已经可用的证据。 -本次仅恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`;滤镜算法、49个插件参数、48个发布入口、Type=3/Drop Zone=1及版本1.0.1均保留。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。 +本次仅恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`;滤镜算法、49个插件参数、48个发布入口、Type=3/Drop Zone=1及版本1.0.2均保留。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。 本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后的FCP新增实例、输入替换、Mix=0直通及实际人像渲染需由主任务重新实测。不得将“恢复链条”描述为已经通过FCP黑屏复测。 diff --git a/Templates/大鱼人像美化/VALIDATION.json b/Templates/大鱼人像美化/VALIDATION.json index e21b241..1a74251 100644 --- a/Templates/大鱼人像美化/VALIDATION.json +++ b/Templates/大鱼人像美化/VALIDATION.json @@ -1,6 +1,6 @@ { - "version": "1.0.1", - "sha256": "2f7d1e0bf91a6dab82f5dde0a20d971a75abd2056a48104d861e9c112870aae4", + "version": "1.0.2", + "sha256": "fe637290212481c34cbfd8f4de5bf09101a3fbe88f4db3e55b35d5375f3ac1ba", "filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "publishedFunctionalControls": 46, "informationalControls": 2, diff --git a/Templates/大鱼人像美化/大鱼人像美化.moef b/Templates/大鱼人像美化/大鱼人像美化.moef index 93e12d9..0d9ebec 100644 --- a/Templates/大鱼人像美化/大鱼人像美化.moef +++ b/Templates/大鱼人像美化/大鱼人像美化.moef @@ -46,7 +46,7 @@ 1 - 大鱼人像美化 1.0.1:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。 + 大鱼人像美化 1.0.2:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。 1920 @@ -298,7 +298,7 @@ - + 8589934608 diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist index c178bf6..68bcb1e 100644 --- a/beatygril/beatygril/Plugin/Info.plist +++ b/beatygril/beatygril/Plugin/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.0.1 + 1.0.2 CFBundleVersion - 3 + 4 NSHumanReadableCopyright PlugInKit @@ -72,7 +72,7 @@ uuid FB14B0E6-59B2-4836-916F-CD0C559E9241 version - 1.0.1 + 1.0.2 className @@ -84,7 +84,7 @@ uuid E0E432C8-3D71-4ABA-B006-E7F519E06C87 version - 1.0.1 + 1.0.2 protocolNames FxOnScreenControl diff --git a/beatygril/beatygril/Plugin/PGBeautyRenderer.h b/beatygril/beatygril/Plugin/PGBeautyRenderer.h index c241837..a0a6965 100644 --- a/beatygril/beatygril/Plugin/PGBeautyRenderer.h +++ b/beatygril/beatygril/Plugin/PGBeautyRenderer.h @@ -14,6 +14,14 @@ NS_ASSUME_NONNULL_BEGIN 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 diff --git a/beatygril/beatygril/Plugin/PGBeautyRenderer.m b/beatygril/beatygril/Plugin/PGBeautyRenderer.m index 4d31ead..4c9fc6f 100644 --- a/beatygril/beatygril/Plugin/PGBeautyRenderer.m +++ b/beatygril/beatygril/Plugin/PGBeautyRenderer.m @@ -82,6 +82,13 @@ static NSDictionary *PGKernels(void) { "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)); }"); + 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;" @@ -133,6 +140,11 @@ static NSDictionary *PGKernels(void) { - (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) || @@ -145,7 +157,7 @@ static NSDictionary *PGKernels(void) { float mix = PGClamp(s.mix, 0, 1); if (mix == 0 && !s.showMask) return image; NSDictionary *k = PGKernels(); - if (k.count != 10) { + if (k.count != 11) { if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2 userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}]; return nil; @@ -188,6 +200,18 @@ static NSDictionary *PGKernels(void) { float invert = s.debugChannel==1 || s.debugChannel==2 ? (s.invertMask ? 1 : 0) : 0; return PGApply(k[@"debug"],extent,@[image,preview,@(invert)]); } + // 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) { + 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. @@ -207,12 +231,12 @@ static NSDictionary *PGKernels(void) { 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,mask,@1]); + 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,mask,@1]); + 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); diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.h b/beatygril/beatygril/Plugin/PGFaceProcessor.h index 74307bb..01c3a35 100644 --- a/beatygril/beatygril/Plugin/PGFaceProcessor.h +++ b/beatygril/beatygril/Plugin/PGFaceProcessor.h @@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN /// observations retain their bounding box; render geometry is stored separately. @property (nonatomic, copy, readonly) NSArray *faces; @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; diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.m b/beatygril/beatygril/Plugin/PGFaceProcessor.m index ba361e4..5216715 100644 --- a/beatygril/beatygril/Plugin/PGFaceProcessor.m +++ b/beatygril/beatygril/Plugin/PGFaceProcessor.m @@ -10,6 +10,7 @@ static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face"; @interface PGFaceFrame () @property (nonatomic, copy, readwrite) NSArray *faces; @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; @@ -340,15 +341,17 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) { 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.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.teethMask || !frame.eyeMask) { + 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; } diff --git a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m index e6ebc07..7de0903 100644 --- a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m +++ b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m @@ -2,9 +2,12 @@ #import #import #import "PGParameterIDs.h" +#include @interface PGFaceSelectOSC : NSObject @property (assign) id apiManager; +@property BOOL dragging; +@property BOOL undoOpen; @end @implementation PGFaceSelectOSC - (instancetype)initWithAPIManager:(id)api { @@ -14,21 +17,33 @@ - (id)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];} - (BOOL)selectionEnabledAtTime:(CMTime)time { BOOL selected=NO;id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; - [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time];return selected; + return api && [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time] && selected; +} +- (BOOL)validObjectBounds:(CGRect *)bounds { + id api=[self oscAPI]; + if(!api || !bounds) return NO; + CGRect b=[api objectBounds]; + if(!isfinite(b.origin.x)||!isfinite(b.origin.y)||!isfinite(b.size.width)||!isfinite(b.size.height)||b.size.width<=0||b.size.height<=0) return NO; + *bounds=b;return YES; } - (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time { if(!tile.ioSurface||width<=0||height<=0) return; - CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,width,height)]; - if([self selectionEnabledAtTime:time]) { + 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)]; + CGRect bounds=CGRectZero; + if([self selectionEnabledAtTime:time] && [self validObjectBounds:&bounds]) { double x=.5,y=.5; id params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; - [params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time]; - CGRect bounds=[[self oscAPI] objectBounds];double canvasX=width*.5,canvasY=height*.5; - [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY]; - CGFloat scale=MAX(1,[[self oscAPI] backingScaleFactor]); - size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface); - CGFloat sx=(CGFloat)surfaceW/width,sy=(CGFloat)surfaceH/height; - canvasX*=sx;canvasY*=sy;CGFloat radius=10*scale; + BOOL valid=[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time] && isfinite(x) && isfinite(y); + double canvasX=NAN,canvasY=NAN; + if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY]; + // CANVAS coordinates already address the OSC surface. Callback width and + // height may describe the effect image and must not rescale this 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; @@ -36,6 +51,7 @@ 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); @@ -43,21 +59,38 @@ [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) *part=([self selectionEnabledAtTime:time]&&CGRectContainsPoint([[self oscAPI] objectBounds],CGPointMake(x,y)))?1:0; + if(!part) return;*part=0;CGRect bounds=CGRectZero; + if(isfinite(x)&&isfinite(y)&&[self selectionEnabledAtTime:time]&&[self validObjectBounds:&bounds]&&CGRectContainsPoint(bounds,CGPointMake(x,y))) *part=1; } - (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update { - CGRect bounds=[[self oscAPI] objectBounds];if(bounds.size.width<=0||bounds.size.height<=0) return; + if(update) *update=NO; + CGRect bounds=CGRectZero; + if(!isfinite(x)||!isfinite(y)||![self validObjectBounds:&bounds]) return; id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; BOOL ok=[api setXValue:MIN(1,MAX(0,(x-bounds.origin.x)/bounds.size.width)) YValue:MIN(1,MAX(0,(y-bounds.origin.y)/bounds.size.height)) toParameter:PGSelection atTime:time]; if(update) *update=ok; } - (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time { - id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo startUndoGroup:@"选择人脸"]; - [self setSelectionX:x y:y time:time update:update]; + if(update) *update=NO; + if(part!=1 || !isfinite(x) || !isfinite(y) || ![self selectionEnabledAtTime:time]) return; + CGRect bounds=CGRectZero;if(![self validObjectBounds:&bounds]) return; + id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)]; + if(_undoOpen) [undo endUndoGroup]; + _undoOpen=[undo startUndoGroup:@"选择人脸"]; + BOOL changed=NO;[self setSelectionX:x y:y 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; + if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update]; } -- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {[self setSelectionX:x y:y time:time update:update];} - (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time { - [self setSelectionX:x y:y time:time update:update];id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup]; + if(update) *update=NO; + if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update]; + _dragging=NO; + if(_undoOpen) {id 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;} diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.m b/beatygril/beatygril/Plugin/beatygrilPlugIn.m index f6e9dd5..3f14895 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.m +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.m @@ -250,7 +250,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { 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 teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError]; + 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; } diff --git a/beatygril/beatygril/Wrapper Application/Info.plist b/beatygril/beatygril/Wrapper Application/Info.plist index c55ff47..1460397 100644 --- a/beatygril/beatygril/Wrapper Application/Info.plist +++ b/beatygril/beatygril/Wrapper Application/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.1 + 1.0.2 CFBundleVersion - 3 + 4 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/docs/analysis/REA-分析与复刻说明.md b/docs/analysis/REA-分析与复刻说明.md index f235713..d871318 100644 --- a/docs/analysis/REA-分析与复刻说明.md +++ b/docs/analysis/REA-分析与复刻说明.md @@ -20,6 +20,10 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无 |人脸与跟踪|Vision landmarks、原版 Metal 位移网格、OpenCV 动态库、历史脸框/关键点、选择及分析数据均有证据。`renderDestinationImage:` 及函数检索见插件证据包|部分回答;运动遮挡、切镜与完整身份逻辑未等价验证| |像素一致性|未获得原版相同输入/参数的可信渲染对照|未验证,不能声称逐像素一致| +## 原版 FCP 模板的实际发布范围 + +补充检查用户指定的 `~/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef`:共15个发布入口,包括空名自定义面板入口、脸型、脸框预览、白牙、亮眼和混合,没有跟踪目标、跟踪开关或前向/后向分析按钮。清单和模板摘要见 [发布参数记录](reference-template-parameters.json)。二进制内部的分析/跟踪代码不能据此解释成当前FCP模板已开放的功能;本项目的三目标分析与跟踪是独立扩展,没有完成原版同类功能的用户界面或运动等价验证。 + ## 已实现的功能 - 中频磨皮、强度、半径、高频细节平滑、锐化、五官保护及各大功能分组开关。 @@ -27,7 +31,7 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无 - 牙齿美白和眼睛提亮,基于 Vision 面部区域构建软蒙版。 - 瘦脸、大眼、短脸、窄脸、下巴、V 脸、下颌、颧骨、小头和小头范围。 - 全脸/选中脸、画面点击选脸、多人高速检测、防跳脸范围、脸框和跟踪框。 -- 三个独立跟踪目标,向前/向后分析,清除当前目标;精确 CMTime 数据持久化到宿主工程,可逆序和任意顺序读取。 +- 作为扩展提供三个独立跟踪目标,向前/向后分析,清除当前目标;精确 CMTime 数据持久化到宿主工程,可逆序和任意顺序读取。 - 蒙版阈值/柔度/边缘保护、皮肤/牙齿/眼睛通道预览与反转、包含几何变形的整体混合。 - 保留透明度、完整输入范围、非零图像原点与宿主上下原点;原生 FxPlug 参数可由宿主保存和制作关键帧。 diff --git a/docs/analysis/reference-template-parameters.json b/docs/analysis/reference-template-parameters.json new file mode 100644 index 0000000..9947e2f --- /dev/null +++ b/docs/analysis/reference-template-parameters.json @@ -0,0 +1,83 @@ +{ + "path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef", + "sha256": "5c20b387f6a6438614a9182d3f93f67473881d433a25f7c3b557d063f55a5d92", + "published_count": 15, + "published": [ + { + "object": "10030", + "channel": "./210", + "name": "" + }, + { + "object": "10030", + "channel": "./18", + "name": "瘦脸" + }, + { + "object": "10030", + "channel": "./33", + "name": "大眼" + }, + { + "object": "10030", + "channel": "./22", + "name": "短脸" + }, + { + "object": "10030", + "channel": "./23", + "name": "窄脸" + }, + { + "object": "10030", + "channel": "./24", + "name": "下巴" + }, + { + "object": "10030", + "channel": "./25", + "name": "V脸" + }, + { + "object": "10030", + "channel": "./26", + "name": "下颚骨" + }, + { + "object": "10030", + "channel": "./27", + "name": "颧骨" + }, + { + "object": "10030", + "channel": "./28", + "name": "小头" + }, + { + "object": "10030", + "channel": "./29", + "name": "小头范围" + }, + { + "object": "10030", + "channel": "./19", + "name": "人脸框预览" + }, + { + "object": "10030", + "channel": "./31", + "name": "白牙" + }, + { + "object": "10030", + "channel": "./32", + "name": "亮眼" + }, + { + "object": "10030", + "channel": "./10001", + "name": "混合" + } + ], + "tracking_controls_published": false +} diff --git a/scripts/deliver.py b/scripts/deliver.py index 75f61ed..1ab7b19 100755 --- a/scripts/deliver.py +++ b/scripts/deliver.py @@ -45,6 +45,13 @@ try: raise RuntimeError('Apple did not accept notarization; original installed app retained') run(['xcrun','stapler','staple',str(app)]) run(['xcrun','stapler','validate',str(app)]) + # A live host retains its old XPC/OSC connection. Replacing the bundle while + # that connection exists can deadlock host shutdown or select stale code. + running=run(['ps','-axo','comm=']).splitlines() + hosts={'/Applications/Motion.app/Contents/MacOS/Motion', + '/Applications/Final Cut Pro.app/Contents/MacOS/Final Cut Pro'} + if hosts.intersection(line.strip() for line in running): + raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained') if installed.exists(): info=plistlib.loads((installed/'Contents/Info.plist').read_bytes()) if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('The installation path belongs to another product') @@ -60,6 +67,9 @@ try: if info.get('CFBundleIdentifier')!=bundle_id or 'TeamIdentifier=B3W25FX5H5' not in signature:raise RuntimeError('Installed app identity mismatch') (logs/'installed-signature.txt').write_text(signature) run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')]) + # Xcode may register its build product. Only the installed plugin should + # be discoverable while native host validation is in progress. + run(['pluginkit','-r',str(plugin)]) except Exception: if installed.exists():shutil.rmtree(installed) if previous.exists():shutil.move(str(previous),str(installed)) diff --git a/scripts/test-osc.sh b/scripts/test-osc.sh new file mode 100755 index 0000000..ffaed0b --- /dev/null +++ b/scripts/test-osc.sh @@ -0,0 +1,20 @@ +#!/bin/zsh +set -euo pipefail +cd "${0:A:h:h}" +output_dir=$(mktemp -d /tmp/paintfcp-osc-tests.XXXXXX) +trap 'rm -rf "$output_dir"' EXIT +plugin_dir="$PWD/beatygril/beatygril/Plugin" +sdk_frameworks=/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks +runtime_frameworks=/Library/Developer/Frameworks +compiler_extra=() +if [[ "${PAINTFCP_OSC_ASAN:-0}" == 1 ]]; then + compiler_extra=(-fsanitize=address -g) +fi +xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \ + -mmacosx-version-min=13.0 -I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \ + -Wl,-rpath,"$runtime_frameworks" \ + "$plugin_dir/PGFaceSelectOSC.m" tests/osc_tests.m \ + -framework Foundation -framework CoreImage -framework CoreGraphics \ + -framework CoreMedia -framework CoreVideo -framework IOSurface \ + -framework FxPlug -framework PluginManager -o "$output_dir/osc-tests" +"$output_dir/osc-tests" "$plugin_dir/Info.plist" diff --git a/tests/beauty_render_tests.m b/tests/beauty_render_tests.m index 98a7264..24050fb 100644 --- a/tests/beauty_render_tests.m +++ b/tests/beauty_render_tests.m @@ -103,6 +103,30 @@ int main(void) { @autoreleasepool { s=PGBeautyDefaults();s.midtoneDetail=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.featureProtection=1; float *protected=Render(context,[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:half error:&error],extent); Check(Difference(base,protected,6,40)<0.00001 && Texture(protected,56,88)0.08 && Difference(base,continuous,56,88)>0.08,@"Skin exposure does not leave dark detail-protection holes"); + s.featureProtection=0; + float *unprotectedColor=Render(context,[renderer processImage:source settings:s skinMask:half colorMask:white teethMask:half eyeMask:half error:&error],extent); + Check(Difference(continuous,unprotectedColor,4,W-4)<0.00001,@"Feature protection changes detail, not face exposure coverage"); + free(continuous);free(unprotectedColor); + CIImage *flatSkin=[[CIImage imageWithColor:[CIColor colorWithRed:0.56 green:0.40 blue:0.31 alpha:1]] imageByCroppingToRect:extent]; + float *flatPixels=Render(context,flatSkin,extent); + float *feathered=Render(context,[renderer processImage:flatSkin settings:s skinMask:half colorMask:half teethMask:nil eyeMask:nil error:&error],extent); + float largestStep=0; + for(int x=42;x<54;x++) { + int i=(32*W+x)*4,j=i+4; + largestStep=fmaxf(largestStep,fabsf((feathered[i]-flatPixels[i])-(feathered[j]-flatPixels[j]))); + } + if(largestStep>=0.09) fprintf(stderr,"Face exposure boundary maximum step: %.6f\n",largestStep); + Check(largestStep<0.09,@"Face color coverage feathers a hard support boundary without a visible exposure step"); + Check(Difference(flatPixels,feathered,64,88)<0.00001,@"Feathered face exposure stays local");free(feathered);free(flatPixels); + CIImage *blueHair=[[CIImage imageWithColor:[CIColor colorWithRed:0.03 green:0.16 blue:0.70 alpha:1]] imageByCroppingToRect:extent]; + float *blueBefore=Render(context,blueHair,extent); + float *blueAfter=Render(context,[renderer processImage:blueHair settings:s skinMask:white colorMask:white teethMask:nil eyeMask:nil error:&error],extent); + Check(Difference(blueBefore,blueAfter,4,W-4)<0.00001,@"Skin exposure excludes saturated blue hair within face support");free(blueBefore);free(blueAfter); s=PGBeautyDefaults();s.showMask=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.maskThreshold=255; float *strictMask=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent); s.maskThreshold=0; diff --git a/tests/face_tests.m b/tests/face_tests.m index c2fda37..e1b7420 100644 --- a/tests/face_tests.m +++ b/tests/face_tests.m @@ -79,6 +79,8 @@ int main(int argc, const char *argv[]) { PGBeautySettings unprotected = settings; unprotected.featureProtection = 0; PGFaceFrame *withoutProtection = [processor processImage:image settings:unprotected time:kCMTimeZero error:&error]; Check(Channel(context, frame.skinMask, 23, 71, 0) < Channel(context, withoutProtection.skinMask, 23, 71, 0), @"feature protection removes eye from skin smoothing"); + Check([Pixels(context,frame.colorMask) isEqualToData:Pixels(context,withoutProtection.colorMask)], @"color support remains continuous when detail protection is toggled"); + Check(Channel(context,frame.colorMask,23,71,0)>Channel(context,frame.skinMask,23,71,0), @"color support retains coverage inside eye protection cutouts"); settings.singleFace = 1; settings.selectionX = 0.78; settings.selectionY = 0.45; frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error]; diff --git a/tests/osc_tests.m b/tests/osc_tests.m new file mode 100644 index 0000000..8b74ebc --- /dev/null +++ b/tests/osc_tests.m @@ -0,0 +1,116 @@ +#import +#import +#import +#import +#import +#import "PGParameterIDs.h" +#include + +@interface PGFaceSelectOSC : NSObject +- (instancetype)initWithAPIManager:(id)api; +@end + +@interface PGOSCAPI : NSObject +@property BOOL enabled, rejectRead, rejectPoint, rejectWrite, rejectUndo; +@property BOOL disableOSC, disableParams, disableSetting, disableUndo; +@property CGRect bounds; +@property double x,y,backing; +@property NSUInteger writes,undoStarts,undoEnds; +@property double lastX,lastY; +@property CMTime lastTime; +@end +@implementation PGOSCAPI +- (instancetype)init {if((self=[super init])){_enabled=YES;_bounds=CGRectMake(-100,-50,200,100);_x=.25;_y=.75;_backing=1;}return self;} +- (id)apiForProtocol:(Protocol *)protocol { + NSString *name=NSStringFromProtocol(protocol); + if([name isEqual:@"FxOnScreenControlAPI_v4"])return _disableOSC?nil:self; + if([name isEqual:@"FxParameterRetrievalAPI_v6"])return _disableParams?nil:self; + if([name isEqual:@"FxParameterSettingAPI_v5"])return _disableSetting?nil:self; + if([name isEqual:@"FxUndoAPI"])return _disableUndo?nil:self; + return nil; +} +- (BOOL)getBoolValue:(BOOL *)value fromParameter:(UInt32)identifier atTime:(CMTime)time { + if(identifier!=PGSingleFace||_rejectRead)return NO;*value=_enabled;return YES; +} +- (BOOL)getXValue:(double *)x YValue:(double *)y fromParameter:(UInt32)identifier atTime:(CMTime)time { + if(identifier!=PGSelection||_rejectPoint)return NO;*x=_x;*y=_y;return YES; +} +- (BOOL)setXValue:(double)x YValue:(double)y toParameter:(UInt32)identifier atTime:(CMTime)time { + if(identifier!=PGSelection||_rejectWrite)return NO; + _writes++;_lastX=x;_lastY=y;_lastTime=time;return YES; +} +- (CGRect)objectBounds {return _bounds;} +- (double)backingScaleFactor {return _backing;} +- (void)convertPointFromSpace:(FxDrawingCoordinates)from fromX:(double)x fromY:(double)y toSpace:(FxDrawingCoordinates)to toX:(double *)tx toY:(double *)ty { + *tx=x+200;*ty=y+100; +} +- (BOOL)startUndoGroup:(NSString *)name {_undoStarts++;return !_rejectUndo;} +- (BOOL)endUndoGroup {_undoEnds++;return YES;} +@end + +@interface PGOSCTile : FxImageTile +@property (strong) IOSurface *surface; +@property FxImageOrigin origin; +@end +@implementation PGOSCTile +- (IOSurface *)ioSurface {return _surface;} +- (FxImageOrigin)imageOrigin {return _origin;} +- (CGColorSpaceRef)colorSpace {return nil;} +@end +static NSUInteger Checks=0,Failures=0; +static void Check(BOOL value,NSString *message) {Checks++;if(!value){Failures++;fprintf(stderr,"FAIL: %s\n",message.UTF8String);}} +static PGOSCTile *Tile(void) { + IOSurfaceRef surface=IOSurfaceCreate((__bridge CFDictionaryRef)@{(id)kIOSurfaceWidth:@320,(id)kIOSurfaceHeight:@240,(id)kIOSurfaceBytesPerElement:@4,(id)kIOSurfaceBytesPerRow:@1280,(id)kIOSurfacePixelFormat:@(kCVPixelFormatType_32BGRA),(id)kIOSurfaceAllocSize:@(320*240*4)}); + PGOSCTile *tile=[PGOSCTile new];tile.surface=CFBridgingRelease(surface);return tile; +} +static float AlphaAt(PGOSCTile *tile,int x,int y) { + CIImage *image=[CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.surface options:@{kCIImageColorSpace:NSNull.null}]; + CIContext *context=[CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}]; + float p[4]={0};[context render:image toBitmap:p rowBytes:sizeof(p) bounds:CGRectMake(x,y,1,1) format:kCIFormatRGBAf colorSpace:nil];return p[3]; +} +int main(int argc,const char **argv) {@autoreleasepool { + PGOSCAPI *api=[PGOSCAPI new];PGFaceSelectOSC *osc=[[PGFaceSelectOSC alloc] initWithAPIManager:api]; + CMTime time=CMTimeMake(121,24);NSInteger part=-1;BOOL update=YES,handled=YES; + Check([osc conformsToProtocol:@protocol(FxOnScreenControl_v4)],@"OSC class conforms to the real FxPlug v4 protocol"); + NSArray *required=@[@"drawingCoordinates",@"drawOSCWithWidth:height:activePart:destinationImage:atTime:",@"hitTestOSCAtMousePositionX:mousePositionY:activePart:atTime:",@"mouseDownAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseDraggedAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseUpAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"keyDownAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:",@"keyUpAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:"]; + for(NSString *selector in required)Check([osc respondsToSelector:NSSelectorFromString(selector)],[NSString stringWithFormat:@"Required callback exists: %@",selector]); + Check(osc.drawingCoordinates==kFxDrawingCoordinates_OBJECT,@"Mouse callbacks use declared object coordinates"); + NSDictionary *info=argc>1 ? [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithUTF8String:argv[1]]] : nil; + NSArray *entries=info[@"ProPlugPlugInList"];NSDictionary *control=nil;NSMutableArray *filters=[NSMutableArray new];NSMutableSet *ids=[NSMutableSet new];BOOL unique=YES; + for(NSDictionary *entry in entries){if([ids containsObject:entry[@"uuid"]])unique=NO;[ids addObject:entry[@"uuid"]];if([entry[@"protocolNames"] containsObject:@"FxOnScreenControl"])control=entry;if([entry[@"protocolNames"] containsObject:@"FxFilter"])[filters addObject:entry];} + Check(unique && control && filters.count==1,@"Registration has unique filter and dedicated OSC identities"); + Check([control[@"className"] isEqual:@"PGFaceSelectOSC"] && NSClassFromString(control[@"className"])==PGFaceSelectOSC.class,@"Registered OSC resolves to the compiled class"); + Check([control[@"supportedPlugins"] isEqual:@[filters.firstObject[@"uuid"]]],@"Dedicated OSC links to the released filter UUID"); + api.enabled=NO;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Disabled selection does not claim mouse events"); + [osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0 && api.undoStarts==0,@"Disabled mouse-down writes nothing and opens no undo group"); + api.enabled=YES;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==1,@"Enabled selection accepts a finite point in object bounds"); + [osc hitTestOSCAtMousePositionX:1000 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Outside-object hit test is empty"); + [osc hitTestOSCAtMousePositionX:NAN mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Nonfinite hit coordinates are rejected"); + [osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:NULL atTime:time];Check(YES,@"Null hit-test output is safe"); + [osc mouseDownAtPositionX:0 positionY:0 activePart:0 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0,@"Inactive part does not start selection"); + update=YES;[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoEnds==0,@"Mouse-up without mouse-down leaves undo balanced"); + [osc mouseDownAtPositionX:-50 positionY:25 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && fabs(api.lastX-.25)<1e-8 && fabs(api.lastY-.75)<1e-8,@"Mouse-down normalizes nonzero object bounds"); + Check(CMTimeCompare(api.lastTime,time)==0 && api.undoStarts==1,@"Selection writes use callback time and open one undo group"); + [osc mouseDraggedAtPositionX:500 positionY:-500 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.lastX==1 && api.lastY==0,@"Dragging clamps normalized selection to the source"); + [osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.undoEnds==1,@"Gesture completes its matching undo group"); + NSUInteger writes=api.writes;[osc mouseDraggedAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Stray drag after release writes nothing"); + api.bounds=CGRectMake(0,0,NAN,100);[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Invalid host bounds produce an empty hit"); + update=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Invalid bounds initialize forceUpdate and avoid parameter writes");api.bounds=CGRectMake(-100,-50,200,100); + api.rejectWrite=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoStarts==api.undoEnds,@"Rejected parameter write closes the opened undo group");api.rejectWrite=NO; + api.rejectUndo=YES;NSUInteger ends=api.undoEnds;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(api.undoEnds==ends,@"Failed undo start is never paired with an invalid end");api.rejectUndo=NO; + for(int disabled=0;disabled<3;disabled++){ + api.disableOSC=disabled==0;api.disableParams=disabled==1;api.disableSetting=disabled==2; + writes=api.writes;update=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,[NSString stringWithFormat:@"Missing host API %d is handled safely",disabled]); + } + api.disableOSC=api.disableParams=api.disableSetting=NO; + [osc keyDownAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-down clears outputs");update=handled=YES; + [osc keyUpAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-up clears outputs"); + PGOSCTile *tile=Tile();api.x=.25;api.y=.75; + [osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time]; + Check(AlphaAt(tile,150,125)>.9,@"Canvas conversion directly positions the visible handle despite effect/surface size mismatch"); + Check(AlphaAt(tile,25,28)<.01,@"OSC surface is not incorrectly rescaled by effect image dimensions"); + api.enabled=NO;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)<.01,@"Disabled drawing clears the prior handle"); + api.enabled=YES;api.backing=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)>.9,@"Nonfinite backing scale uses a finite fallback"); + api.x=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)<.01,@"Nonfinite stored selection does not create invalid drawing filters"); + printf("OSC adapter: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0; +}}