@@ -18,11 +18,13 @@ @implementation XCWChromeRenderer
1818 NSDictionary *json = chromeInfo[@" json" ];
1919 NSDictionary *images = [json[@" images" ] isKindOfClass: [NSDictionary class ]] ? json[@" images" ] : @{};
2020 NSDictionary *sizing = [images[@" sizing" ] isKindOfClass: [NSDictionary class ]] ? images[@" sizing" ] : @{};
21+ NSDictionary *stand = [images[@" stand" ] isKindOfClass: [NSDictionary class ]] ? images[@" stand" ] : @{};
2122
22- CGFloat insetTop = [self numberValue: sizing[@" topHeight" ]];
23- CGFloat insetLeft = [self numberValue: sizing[@" leftWidth" ]];
24- CGFloat insetBottom = [self numberValue: sizing[@" bottomHeight" ]];
25- CGFloat insetRight = [self numberValue: sizing[@" rightWidth" ]];
23+ CGFloat sizingTop = [self numberValue: sizing[@" topHeight" ]];
24+ CGFloat sizingLeft = [self numberValue: sizing[@" leftWidth" ]];
25+ CGFloat sizingBottom = [self numberValue: sizing[@" bottomHeight" ]];
26+ CGFloat sizingRight = [self numberValue: sizing[@" rightWidth" ]];
27+ CGFloat standHeight = [self numberValue: stand[@" height" ]];
2628
2729 CGSize compositeSize = [self compositeSizeForChromeInfo: chromeInfo error: error];
2830 if (CGSizeEqualToSize (compositeSize, CGSizeZero)) {
@@ -31,19 +33,59 @@ @implementation XCWChromeRenderer
3133
3234 NSDictionary *paths = [json[@" paths" ] isKindOfClass: [NSDictionary class ]] ? json[@" paths" ] : @{};
3335 NSDictionary *border = [paths[@" simpleOutsideBorder" ] isKindOfClass: [NSDictionary class ]] ? paths[@" simpleOutsideBorder" ] : @{};
36+ NSDictionary *borderInsets = [border[@" insets" ] isKindOfClass: [NSDictionary class ]] ? border[@" insets" ] : @{};
3437 CGFloat rawCornerRadius = [self numberValue: border[@" cornerRadiusX" ]];
3538
39+ CGFloat borderTop = [self numberValue: borderInsets[@" top" ]];
40+ CGFloat borderLeft = [self numberValue: borderInsets[@" left" ]];
41+ CGFloat borderBottom = [self numberValue: borderInsets[@" bottom" ]];
42+ CGFloat borderRight = [self numberValue: borderInsets[@" right" ]];
43+
44+ CGFloat bezelTop = sizingTop + borderTop;
45+ CGFloat bezelLeft = sizingLeft + borderLeft;
46+ CGFloat bezelBottom = sizingBottom + borderBottom;
47+ CGFloat bezelRight = sizingRight + borderRight;
48+
3649 BOOL watchProfile = [self isWatchProfile: plist];
50+ BOOL hasComposite = [self compositeAssetPathForChromeInfo: chromeInfo].length > 0 ;
3751 CGFloat screenScale = MAX ([self numberValue: plist[@" mainScreenScale" ]], 1.0 );
3852 CGFloat profileScreenWidth = [self numberValue: plist[@" mainScreenWidth" ]];
3953 CGFloat profileScreenHeight = [self numberValue: plist[@" mainScreenHeight" ]];
4054 CGFloat pointScreenWidth = watchProfile ? profileScreenWidth : profileScreenWidth / screenScale;
41- CGFloat screenWidth = watchProfile ? profileScreenWidth : MAX (compositeSize.width - insetLeft - insetRight, 1.0 );
42- CGFloat screenHeight = watchProfile ? profileScreenHeight : MAX (compositeSize.height - insetTop - insetBottom, 1.0 );
43- CGFloat screenX = watchProfile ? MAX ((compositeSize.width - screenWidth) / 2.0 , 0.0 ) : insetLeft;
44- CGFloat screenY = watchProfile ? MAX ((compositeSize.height - screenHeight) / 2.0 , 0.0 ) : insetTop;
45- CGFloat bezelWidth = MAX (insetLeft, insetTop);
46- CGFloat innerRadius = MAX (rawCornerRadius - bezelWidth, 0.0 );
55+ CGFloat pointScreenHeight = watchProfile ? profileScreenHeight : profileScreenHeight / screenScale;
56+
57+ CGFloat screenWidth;
58+ CGFloat screenHeight;
59+ CGFloat screenX;
60+ CGFloat screenY;
61+ if (hasComposite && pointScreenWidth > 0.0 && pointScreenHeight > 0.0 ) {
62+ // The composite PDF defines authoritative chrome dimensions; the screen is the
63+ // device's point size centered horizontally inside the chrome with the bezel
64+ // insets pushing it down vertically (and stand area, if any, occupying the
65+ // bottom of the composite).
66+ screenWidth = pointScreenWidth;
67+ screenHeight = pointScreenHeight;
68+ screenX = MAX ((compositeSize.width - screenWidth) / 2.0 , 0.0 );
69+ CGFloat usableHeight = compositeSize.height - standHeight;
70+ screenY = MAX ((usableHeight - screenHeight) / 2.0 , bezelTop);
71+ } else if (watchProfile) {
72+ screenWidth = profileScreenWidth;
73+ screenHeight = profileScreenHeight;
74+ screenX = MAX ((compositeSize.width - screenWidth) / 2.0 , 0.0 );
75+ screenY = MAX ((compositeSize.height - screenHeight) / 2.0 , 0.0 );
76+ } else {
77+ // 9-slice path: bezel insets (sizing + simpleOutsideBorder) frame the screen.
78+ // The stand, when present, sits below the chrome and outside the screen rect.
79+ screenX = bezelLeft;
80+ screenY = bezelTop;
81+ screenWidth = MAX (compositeSize.width - bezelLeft - bezelRight, 1.0 );
82+ screenHeight = MAX (compositeSize.height - standHeight - bezelTop - bezelBottom, 1.0 );
83+ }
84+
85+ // Inner corner radius: when the thickest bezel exceeds the outer radius, the
86+ // screen edge is past the curved region and the inner is effectively rectangular
87+ // (e.g. iPhone 6s Plus's tall top/bottom bezel collapses the screen rounding).
88+ CGFloat innerRadius = MAX (rawCornerRadius - MAX (bezelLeft, bezelTop), 0.0 );
4789 CGFloat radiusScale = pointScreenWidth > 0.0 ? screenWidth / pointScreenWidth : 1.0 ;
4890 CGFloat cornerRadius = watchProfile ? rawCornerRadius : innerRadius * radiusScale;
4991
@@ -271,6 +313,9 @@ + (CGSize)compositeSizeForChromeInfo:(NSDictionary *)chromeInfo
271313 NSDictionary *json = chromeInfo[@" json" ];
272314 NSDictionary *images = [json[@" images" ] isKindOfClass: [NSDictionary class ]] ? json[@" images" ] : @{};
273315 NSDictionary *sizing = [images[@" sizing" ] isKindOfClass: [NSDictionary class ]] ? images[@" sizing" ] : @{};
316+ NSDictionary *paths = [json[@" paths" ] isKindOfClass: [NSDictionary class ]] ? json[@" paths" ] : @{};
317+ NSDictionary *bord = [paths[@" simpleOutsideBorder" ] isKindOfClass: [NSDictionary class ]] ? paths[@" simpleOutsideBorder" ] : @{};
318+ NSDictionary *bordI = [bord[@" insets" ] isKindOfClass: [NSDictionary class ]] ? bord[@" insets" ] : @{};
274319 CGFloat screenScale = MAX ([self numberValue: plist[@" mainScreenScale" ]], 1.0 );
275320 BOOL watchProfile = [self isWatchProfile: plist];
276321 CGFloat screenWidth = [self numberValue: plist[@" mainScreenWidth" ]];
@@ -279,8 +324,14 @@ + (CGSize)compositeSizeForChromeInfo:(NSDictionary *)chromeInfo
279324 screenWidth /= screenScale;
280325 screenHeight /= screenScale;
281326 }
282- CGFloat totalWidth = screenWidth + [self numberValue: sizing[@" leftWidth" ]] + [self numberValue: sizing[@" rightWidth" ]];
283- CGFloat totalHeight = screenHeight + [self numberValue: sizing[@" topHeight" ]] + [self numberValue: sizing[@" bottomHeight" ]];
327+ CGFloat bezelLeft = [self numberValue: sizing[@" leftWidth" ]] + [self numberValue: bordI[@" left" ]];
328+ CGFloat bezelRight = [self numberValue: sizing[@" rightWidth" ]] + [self numberValue: bordI[@" right" ]];
329+ CGFloat bezelTop = [self numberValue: sizing[@" topHeight" ]] + [self numberValue: bordI[@" top" ]];
330+ CGFloat bezelBottom = [self numberValue: sizing[@" bottomHeight" ]] + [self numberValue: bordI[@" bottom" ]];
331+ NSDictionary *stand = [images[@" stand" ] isKindOfClass: [NSDictionary class ]] ? images[@" stand" ] : @{};
332+ CGFloat standHeight = [self numberValue: stand[@" height" ]];
333+ CGFloat totalWidth = screenWidth + bezelLeft + bezelRight;
334+ CGFloat totalHeight = screenHeight + bezelTop + bezelBottom + standHeight;
284335 if (totalWidth > 0.0 && totalHeight > 0.0 ) {
285336 return CGSizeMake (totalWidth, totalHeight);
286337 }
@@ -348,7 +399,10 @@ + (BOOL)drawSlicedChromeInfo:(NSDictionary *)chromeInfo
348399 CGFloat bottomHeight = MAX (MAX (MAX (bottom, bottomSize.height ), bottomLeftSize.height ), bottomRightSize.height );
349400 CGFloat rightWidth = MAX (MAX (MAX (right, rightSize.width ), topRightSize.width ), bottomRightSize.width );
350401 CGFloat middleWidth = MAX (size.width - leftWidth - rightWidth, 1.0 );
351- CGFloat middleHeight = MAX (size.height - topHeight - bottomHeight, 1.0 );
402+ NSDictionary *stand = [images[@" stand" ] isKindOfClass: [NSDictionary class ]] ? images[@" stand" ] : @{};
403+ CGFloat standHeight = [self numberValue: stand[@" height" ]];
404+ CGFloat chromeHeight = MAX (size.height - standHeight, 1.0 );
405+ CGFloat middleHeight = MAX (chromeHeight - topHeight - bottomHeight, 1.0 );
352406
353407 NSArray <NSDictionary *> *pieces = @[
354408 @{ @" path" : topLeftPath, @" rect" : [NSValue valueWithRect: NSMakeRect (0 , 0 , leftWidth, topHeight)] },
@@ -377,6 +431,13 @@ + (BOOL)drawSlicedChromeInfo:(NSDictionary *)chromeInfo
377431 return NO ;
378432 }
379433 }
434+ if (standHeight > 0.0 && ![self drawStandImagesForChromeInfo: chromeInfo
435+ inSize: size
436+ chromeYMax: chromeHeight
437+ context: context
438+ error: error]) {
439+ return NO ;
440+ }
380441 if (!drewAny && error != NULL ) {
381442 *error = [NSError errorWithDomain: XCWChromeRendererErrorDomain
382443 code: 13
@@ -387,6 +448,62 @@ + (BOOL)drawSlicedChromeInfo:(NSDictionary *)chromeInfo
387448 return drewAny;
388449}
389450
451+ + (BOOL )drawStandImagesForChromeInfo : (NSDictionary *)chromeInfo
452+ inSize : (CGSize)size
453+ chromeYMax : (CGFloat)chromeYMax
454+ context : (CGContextRef)context
455+ error : (NSError * _Nullable __autoreleasing *)error {
456+ NSDictionary *json = chromeInfo[@" json" ];
457+ NSString *chromePath = chromeInfo[@" chromePath" ];
458+ NSDictionary *images = [json[@" images" ] isKindOfClass: [NSDictionary class ]] ? json[@" images" ] : @{};
459+ NSDictionary *stand = [images[@" stand" ] isKindOfClass: [NSDictionary class ]] ? images[@" stand" ] : @{};
460+ CGFloat standWidth = [self numberValue: stand[@" width" ]];
461+ CGFloat standHeight = [self numberValue: stand[@" height" ]];
462+ if (standWidth <= 0.0 || standHeight <= 0.0 ) {
463+ return YES ;
464+ }
465+
466+ NSString *leftName = [stand[@" left" ] isKindOfClass: [NSString class ]] ? stand[@" left" ] : @" " ;
467+ NSString *centerName = [stand[@" center" ] isKindOfClass: [NSString class ]] ? stand[@" center" ] : @" " ;
468+ NSString *rightName = [stand[@" right" ] isKindOfClass: [NSString class ]] ? stand[@" right" ] : @" " ;
469+ NSString *leftPath = leftName.length > 0 ? [self resolvedChromeAssetPathForName: leftName chromePath: chromePath] : @" " ;
470+ NSString *centerPath = centerName.length > 0 ? [self resolvedChromeAssetPathForName: centerName chromePath: chromePath] : @" " ;
471+ NSString *rightPath = rightName.length > 0 ? [self resolvedChromeAssetPathForName: rightName chromePath: chromePath] : @" " ;
472+ CGSize leftSize = [self PDFPageSizeAtPath: leftPath];
473+ CGSize rightSize = [self PDFPageSizeAtPath: rightPath];
474+ CGFloat leftWidth = MAX (leftSize.width , 0.0 );
475+ CGFloat rightWidth = MAX (rightSize.width , 0.0 );
476+ CGFloat centerWidth = MAX (standWidth - leftWidth - rightWidth, 1.0 );
477+ CGFloat x = MAX ((size.width - standWidth) / 2.0 , 0.0 );
478+ CGFloat y = chromeYMax;
479+
480+ if (leftPath.length > 0 && leftWidth > 0.0 ) {
481+ if (![self drawPDFAtPath: leftPath
482+ inRect: CGRectMake (x, y, leftWidth, standHeight)
483+ context: context
484+ error: error]) {
485+ return NO ;
486+ }
487+ }
488+ if (centerPath.length > 0 ) {
489+ if (![self drawPDFAtPath: centerPath
490+ inRect: CGRectMake (x + leftWidth, y, centerWidth, standHeight)
491+ context: context
492+ error: error]) {
493+ return NO ;
494+ }
495+ }
496+ if (rightPath.length > 0 && rightWidth > 0.0 ) {
497+ if (![self drawPDFAtPath: rightPath
498+ inRect: CGRectMake (x + leftWidth + centerWidth, y, rightWidth, standHeight)
499+ context: context
500+ error: error]) {
501+ return NO ;
502+ }
503+ }
504+ return YES ;
505+ }
506+
390507+ (BOOL )drawInputImagesForChromeInfo : (NSDictionary *)chromeInfo
391508 inSize : (CGSize)size
392509 context : (CGContextRef)context
0 commit comments