Version 96 breaks Objective C plugins

Hello!

We have a legacy CocoaScript plugin (we are really early integrators) and have been able to manage this over time by looking at the headers as Sketch progressed, and by reverse engineering what we needed to change. Whenever that was not enough, we’ve had a lot of excellent communication and help from Sketch (huge kudos to Ale Munoz!) as there was no official documentation after version 84.

Our plugin has stopped working since version 96 came out, and after some debugging we traced it to the removal of the class MSSliceTrimming. We were relying on a couple of functions from this class, namely trimSlice and trimmedRectForLayerAncestry to get the position and size of the trimmed slice from the (potentially bigger) layer.

Would someone from Sketch be able to point us to the right direction (which class/functions have replaced this functionality), as it seems that other communication channels might no longer be available? We need to get the position and size (CGRect) of the trimmed image that results from exporting a layer. Thanks in advance!

Hey, it’s nice to meet a fellow CocoaScript/Objective-C plugin developer here!

So MSSliceTrimming class is actually still available in Sketch 96, but it’s been renamed to SketchRendering.MSSliceTrimming (it’s a Swift class now I presume, thus the namespace):

// Dynamic class lookup trying both old and new names in case you
// need to supportolder Sketch versions as well
const SliceTrimmingClass = NSClassFromString("SketchRendering.MSSliceTrimming") ?? NSClassFromString("MSSliceTrimming")
let trimmed = SliceTrimmingClass.trimmedRectForLayerAncestry_(layer.ancestry())

As for +[MSSliceTrimming trimSlice:], I believe it would look like the following when implemented in CocoaScript:

function trimSlice(layer) {
  const SliceTrimmingClass = NSClassFromString("SketchRendering.MSSliceTrimming") ?? NSClassFromString("MSSliceTrimming")
  let trimmed = SliceTrimmingClass.trimmedRectForLayerAncestry_(layer.ancestry())
  layer.setAbsoluteBoundingBox_(trimmed)
  return trimmed
}

Hope it helps!

Wow, I didn’t really hope to find anyone else still using CocoaScript :grinning:

I’ve tried the namespaced version of the class and you’re absolutely right, trimmedRectForLayerAncestry can be called like this!!

The downside is that the rect is still the full layer size. For reference, the example I use is a text layer that’s sized to be greater than the text itself, so the bottom margin gets cropped, and the text is center aligned, so it also crops a bit on each side on the exported image.

In the original code we first called trimSlice on the layer (returning void) which probably set the absolute bounding box internally, and then trimmedRectForLayerAncestry returned the new rect.

Thanks for the help though, at least we located one of the 2 functions!!

Right, it seems that something about whitespace trimming have changed in Sketch 96 and MSSliceTrimming class no longer provides accurate results that match the size of layer renditions exported by Sketch.

I’ve done a quick research and I think I found an alternative method that returns an expected fully trimmed rect for all kind of layers:

  1. First we start with an inexact rect from MSSliceTrimming API:
    let dirty = MSSliceTrimming.trimmedRectForLayerAncestry_(layer.ancestry())
    
  2. Next let’s pretend we’re going to export this layer via MSExporter with empty pixels trimmed:
    // This assumes that "layer" is already marked as exportable
    let request = MSExportRequest.exportRequestsFromLayerAncestry_(layer.ancestry()).firstObject()
    request.setShouldTrim_(true) // 👈 this is key
    
    let exporter = MSExporter.exporterForRequest_colorSpace_(request, nil)
    
  3. Now MSExported has this little useful property trimmedBounds which is basically a combination of 1) a real trimmed layer size and 2) a relative offset of this real trimmed frame within the layer’s untrimmed frame.
    let trimmedSize = exporter.trimmedBounds().size
    let trimmedOriginOffset = exporter.trimmedBounds().origin
    
  4. To get the final real trimmed layer frame we’d need a bit of math to combine the offset we just got with a rect we got earlier from MSSliceTrimming:
    let unalignedResult = NSMakeRect(dirty.origin.x + trimmedOriginOffset.x,
                                     dirty.origin.y + trimmedOriginOffset.y,
                                     trimmedSize.width, trimmedSize.height)
    // (Optional step) The calculated rectangle might not be pixel-aligned by default
    // (i.e. there could be non-integral coordinates or size values like 0.5 or 1.89,
    // and bitmap renditions can only start/end on a full pixel)
    let result = NSIntegralRectWithOptions(unalignedResult, NSAlignAllEdgesOutward)
    

I’ve tested this approach on a bunch of “trimmable” text layers and layer groups – and it seems to be returning the exact same frames Sketch 96 uses when rendering previews for those layers when they’re marked as exportable in the Inspector.

I still don’t really understand why the resulting offset of -[MSExporter trimmedBounds] should be added to the trimmed rect we got from MSSliceTrimming instead of -[MSLayer frame] directly tho, so there might be flaws in my understanding of this whole whitespace trimming machinery :man_shrugging: Please feel free to test it yourself and let me know what I’m missing!

1 Like

This is the same approach we used, but we didn’t think to explicitly call setShouldTrim because we outputted shouldTrim and it was already set to true :exploding_head: Just adding that one line made all the difference and it works.

Thanks so much for the input and help, it was a real life saver!

1 Like

On version 96, there is an issue where if a layer’s parent group has a drop shadow, the position and size are incorrect

If the projection is on the layer itself, the position and size are correct

I can’t quite reproduce this one, can you share a sample design with a problematic layer embedded into a group with a drop shadow?

I’ve tested the following case:

and the text layer’s absolute influence rect calculated with the code above matches the one used by Sketch itself when exporting this layer.

@rodionovd And while this worked for a while, Sketch (probably) did something with a release after 96 that broke masked layers trimming

I had packaged the code into a function

function extractTrimmedSliceBounds(layer) {
    let SliceTrimmingClass = NSClassFromString("SketchRendering.MSSliceTrimming") ?? NSClassFromString("MSSliceTrimming");
    let exportRequestForSlice = MSExportRequest.exportRequestsFromLayerAncestry_(layer.ancestry()).firstObject();
    exportRequestForSlice.setShouldTrim_(true);
    let exporterForSlice = MSExporter.exporterForRequest_colorSpace_(exportRequestForSlice, nil);
    let trimmedRectForLayerAncestry = SliceTrimmingClass.trimmedRectForLayerAncestry_(layer.ancestry())
    let trimmedRect = exporterForSlice.trimmedBounds();
    let trimmedOriginOffset = exporterForSlice.trimmedBounds().origin;
    let unalignedResult = NSMakeRect(trimmedRectForLayerAncestry.origin.x + trimmedOriginOffset.x,
        trimmedRectForLayerAncestry.origin.y + trimmedOriginOffset.y,
        trimmedRect.size.width, trimmedRect.size.height);
    let alignedResult = NSIntegralRectWithOptions(unalignedResult, NSAlignAllEdgesOutward);
    return alignedResult;
}

This now works with normal layers, but even in the simplest masked layer it stops trimming and returns the untrimmed layer size. I have an artboard with a text layer that’s wide, but the actual text in it is much smaller, masked with another layer. It returns the full width. Maybe something to do with the ancestry function.


SurfsApp_sketch-2

Hm, I’ve just set up the same layer hierarchy as you have on the screenshot and extractTrimmedSliceBounds() does return a rectangle that matches the (trimmed) masked area of the text layer precisely for me:

(I’ve assigned the frame returned from extractTrimmedSliceBounds(textLayer) to TrimmedRectResult rectangle on the cavas – you can see its bounds on the second screenshot)

Could you please share a sample document and mention what was the expected trimmed rectangle vs what you actually get?

Here’s the test file I use
mask-sample.sketch (16.4 KB)

The exported PNG is (correctly) 156px width by 17px height.

If I select the “see you on Saturday” layer the properties pane shows the untrimmed size 275px width by 22px height
mask-sample_sketch

The calculated size from the function for the layer is 275x22.

Thanks for the sample file, @georges!

This is what I see when I select the text layer and call extractTrimmedSliceBounds() on it:

I’ve copied the function code verbatim from your message. Tried this on Sketch 99.1 and the latest 99.5 Beta – same behavior on both.

Wondering what could be the reason for such drastic difference in results for you and me :thinking:. I don’t have any third-party plugins enabled, could that be it?

It’s part of an existing plugin, so perhaps something happens earlier in the code that messes the layer up, I’ll see if I can dig it up. Thanks for trying it out though!

1 Like