Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON Object Archives #306

Merged
merged 19 commits into from Dec 20, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions .eslintrc.yml
Expand Up @@ -114,6 +114,16 @@ globals:
MSImmutableSliceLayer: false
MSExportFormat: false
MSTheme: false
MSJSONDataArchiver: false
MSJSONDictionaryUnarchiver: false
MSArchiveHeader: false
NSUTF8StringEncoding: false
MSExportRequest: false
MSExportManager: false
NSFileManager: false
NSDataWritingWithoutOverwriting: false



rules:
###########
Expand Down
22 changes: 22 additions & 0 deletions Source/dom/__tests__/export.test.js
@@ -0,0 +1,22 @@
/* globals expect, test */

import { exportObject, objectFromJSON } from '../export'
import { Shape } from '../layers/Shape'

test('should archive object', () => {
const object = new Shape()
const archive = exportObject(object, { formats: ['json'] })
expect(typeof archive).toBe('object')
expect(archive.type).toBe('ShapePath')
})

test('should maintain object id through archive', () => {
const object = new Shape()
const archive = exportObject(object, {
formats: ['json'],
output: false,
})
const restored = objectFromJSON(archive)
expect(restored.id).toEqual(String(object.id))
expect(restored == object).toBe(true)
})
162 changes: 123 additions & 39 deletions Source/dom/export.js
@@ -1,5 +1,5 @@
import { isWrappedObject } from './utils'
import { Types } from './enums'
import { wrapNativeObject } from './wrapNativeObject'

export const DEFAULT_EXPORT_OPTIONS = {
compact: false,
Expand All @@ -26,11 +26,11 @@ export const DEFAULT_EXPORT_OPTIONS = {
* ### General Options
*
* - use-id-for-name : normally the exported files are given the same names as the layers they represent, but if this options is true, then the layer ids are used instead; defaults to false.
* - output : this is the path of the folder where all exported files are placed; defaults to "~/Documents/Sketch Exports"
* - output : this is the path of the folder where all exported files are placed; defaults to "~/Documents/Sketch Exports". If falsey the data is returned immediately (only supported for json).
* - overwriting : if true, the exporter will overwrite any existing files with new ones; defaults to false.
* - trimmed: if true, any transparent space around the exported image will be trimmed; defaults to false.
* - scales: this should be a list of numbers; it will determine the sizes at which the layers are exported; defaults to "1"
* - formats: this should be a list of one or more of "png", "jpg", "svg", and "pdf"; defaults to "png" (see discussion below)
* - formats: this should be a list of one or more of "png", "jpg", "svg", "json", and "pdf"; defaults to "png" (see discussion below)
*
* ### SVG options
* - compact : if exporting as SVG, this option makes the output more compact; defaults to false.
Expand All @@ -46,58 +46,142 @@ export const DEFAULT_EXPORT_OPTIONS = {
*
*
* @param {dictionary} options Options indicating which sizes and formats to use, etc.
*
* @returns If an output path is not set, the data is returned
*/
export function exportObject(object, options) {
const merged = { ...DEFAULT_EXPORT_OPTIONS, ...options }
if (!object) {
throw new Error('No object provided to export')
}
let { formats } = options
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
if (typeof formats === 'string') {
formats = [formats]
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
}
let exportJSON = false
if (formats) {
const idx = formats.indexOf('json')
if (idx != undefined) {
formats.splice(idx, 1)
exportJSON = true
}
}

const merged = {
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
...DEFAULT_EXPORT_OPTIONS,
...options,
formats,
}

const objectsToExport = (Array.isArray(object) ? object : [object]).map(
o => (isWrappedObject(o) ? o.sketchObject : o)
)
if (!objectsToExport) {
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('No objects provided to export')
}

function archiveNativeObject(nativeObject) {
const archiver = MSJSONDataArchiver.new()
archiver.archiveObjectIDs = true
const aPtr = MOPointer.alloc().init()
const obj = nativeObject.immutableModelObject
? nativeObject.immutableModelObject()
: nativeObject
archiver.archivedDataWithRootObject_error(obj, aPtr)
if (aPtr.value()) {
throw Error('Archive error')
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
}
return archiver.archivedData()
}

// Return data if no output directory specified
if (!merged.output) {
const firstObject = objectsToExport[0]
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
if (exportJSON) {
const str = NSString.alloc().initWithData_encoding(
archiveNativeObject(firstObject),
NSUTF8StringEncoding
)
return JSON.parse(str)
}
// Input code for returning image data here...
throw new Error('Return output is only support for the json format')
}
// Save files to directory at options.output
const exporter = MSSelfContainedHighLevelExporter.alloc().initWithOptions(
merged
)

function exportNativeLayers(layers) {
exporter.exportLayers(layers)
}

function exportNativePage(page) {
exporter.exportPage(page)
}

if (Array.isArray(object)) {
const isArrayOfPages = isWrappedObject(object[0])
? object[0].type === Types.Page
: String(object[0].class()) === 'MSPage'
// Export all object json
if (exportJSON) {
const fm = NSFileManager.defaultManager()
const directory = NSString.stringWithString(
merged.output
).stringByExpandingTildeInPath()
const comps = String(directory).split('/')
fm.createDirectoryAtPath_withIntermediateDirectories_attributes_error(
directory,
true,
null,
null
)

if (isArrayOfPages) {
// support an array of pages
object.forEach(o => {
if (isWrappedObject(o)) {
exportNativePage(o.sketchObject)
} else {
exportNativePage(o)
}
})
} else {
// support an array of layers
exportNativeLayers(
object.map(o => {
if (isWrappedObject(o)) {
return o.sketchObject
}
return o
})
)
objectsToExport.forEach(o => {
const name =
merged['use-id-for-name'] === true || !o.name ? o.objectID() : o.name()
const pathComps = comps.slice()
pathComps.push(`${name}.json`)
const url = NSURL.fileURLWithPath(pathComps.join('/'))
const data = archiveNativeObject(o)
const writeOptions = merged.overwriting
? 0
: NSDataWritingWithoutOverwriting
const ptr = MOPointer.new()
if (!data.writeToURL_options_error(url, writeOptions, ptr)) {
throw new Error(`Error writing json file ${ptr.value()}`)
}
})
// If only JSON stop to bypass the exporters PNG default
if (merged.formats.length == 0) {
WCByrne marked this conversation as resolved.
Show resolved Hide resolved
return true
}
} else if (isWrappedObject(object)) {
// support a wrapped object
if (object.type === Types.Page) {
exportNativePage(object.sketchObject)
}

// Other formats are completed by the exporter
const pages = []
const layers = []
objectsToExport.forEach(o => {
if (String(o.class()) === 'MSPage') {
pages.push(o)
} else {
exportNativeLayers([object.sketchObject])
layers.push(o)
}
} else if (String(object.class()) === 'MSPage') {
// support a native page
exportNativePage(object)
} else {
// support a native layer
exportNativeLayers([object])
})
pages.forEach(p => exportNativePage(p))
exportNativeLayers(layers)
return true
}

export function objectFromJSON(archive, version) {
const v = version || MSArchiveHeader.metadataForNewHeader().version
const ptr = MOPointer.new()
let object = MSJSONDictionaryUnarchiver.unarchiveObjectFromDictionary_asVersion_corruptionDetected_error(
archive,
v,
null,
ptr
)
if (ptr.value()) {
throw new Error(`Failed to create object from sketch JSON: ${ptr.value()}`)
}
if (object.newMutableCounterpart) {
object = object.newMutableCounterpart()
}
return wrapNativeObject(object)
}
5 changes: 4 additions & 1 deletion Source/dom/index.js
@@ -1,7 +1,10 @@
import { AnimationType, BackTarget } from './models/Flow'
import './models/DataOverride'

export { exportObject as export } from './export'
export {
exportObject as export,
objectFromJSON as fromSketchJSON,
} from './export'

export { Document, getDocuments, getSelectedDocument } from './models/Document'
export { Library, getLibraries } from './models/Library'
Expand Down
37 changes: 21 additions & 16 deletions docs/api/export.md
Expand Up @@ -24,21 +24,26 @@ sketch.export(page, options)
sketch.export(document.pages)
```

```javascript
const options = { formats: 'json', output: false }
const sketchJSON = sketch.export(layer, options)
```

Export an object, using the options supplied.

| Parameters | |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| objectToExport<span class="arg-type">[Layer](#layer) / [Layer](#layer)[] / [Page](#page) / [Page](#page)[]</span> | The object to export. |
| options<span class="arg-type">object</span> | Options indicating which sizes and formats to use, etc.. |
| options.output<span class="arg-type">string</span> | this is the path of the folder where all exported files are placed (defaults to `"~/Documents/Sketch Exports"`). |
| options.formats<span class="arg-type">string</span> | Comma separated list of formats to export to (`png`, `jpg`, `svg` or `pdf`) (default to `"png"`). |
| options.scales<span class="arg-type">string</span> | Comma separated list of scales which determine the sizes at which the layers are exported (defaults to `"1"`). |
| options['use-id-for-name']<span class="arg-type">boolean</span> | Name exported images using their id rather than their name (defaults to `false`). |
| options['group-contents-only']<span class="arg-type">boolean</span> | Export only layers that are contained within the group (default to `false`). |
| options.overwriting<span class="arg-type">boolean</span> | Overwrite existing files (if any) with newly generated ones (defaults to `false`). |
| options.trimmed<span class="arg-type">boolean</span> | Trim any transparent space around the exported image (defaults to `false`). |
| options['save-for-web']<span class="arg-type">boolean</span> | If exporting a PNG, remove metadata such as the colour profile from the exported file (defaults to `false`). |
| options.compact<span class="arg-type">boolean</span> | If exporting a SVG, make the output more compact (defaults to `false`). |
| options['include-namespaces']<span class="arg-type">boolean</span> | If exporting a SVG, include extra attributes (defaults to `false`). |
| options.progressive<span class="arg-type">boolean</span> | If exporting a JPG, export a progressive JPEG (only used when exporting to `jpeg`) (defaults to `false`). |
| options.compression<span class="arg-type">number</span> | If exporting a JPG, the compression level to use fo `jpeg` (with `0` being the completely compressed, `1.0` no compression) (defaults to `1.0`). |
| Parameters | |
|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| objectToExport<span class="arg-type">[Layer](#layer) / [Layer](#layer)[] / [Page](#page) / [Page](#page)[]</span> | The object to export. |
| options<span class="arg-type">object</span> | Options indicating which sizes and formats to use, etc.. |
| options.output<span class="arg-type">string</span> | this is the path of the folder where all exported files are placed (defaults to `"~/Documents/Sketch Exports"`). If falsey, the data for the first object/format is returned immmediately (currently only supported for json). |
| options.formats<span class="arg-type">string</span> | Comma separated list of formats to export to (`png`, `jpg`, `svg`, `json` or `pdf`) (default to `"png"`). |
| options.scales<span class="arg-type">string</span> | Comma separated list of scales which determine the sizes at which the layers are exported (defaults to `"1"`). |
| options['use-id-for-name']<span class="arg-type">boolean</span> | Name exported images using their id rather than their name (defaults to `false`). |
| options['group-contents-only']<span class="arg-type">boolean</span> | Export only layers that are contained within the group (default to `false`). |
| options.overwriting<span class="arg-type">boolean</span> | Overwrite existing files (if any) with newly generated ones (defaults to `false`). |
| options.trimmed<span class="arg-type">boolean</span> | Trim any transparent space around the exported image (defaults to `false`). |
| options['save-for-web']<span class="arg-type">boolean</span> | If exporting a PNG, remove metadata such as the colour profile from the exported file (defaults to `false`). |
| options.compact<span class="arg-type">boolean</span> | If exporting a SVG, make the output more compact (defaults to `false`). |
| options['include-namespaces']<span class="arg-type">boolean</span> | If exporting a SVG, include extra attributes (defaults to `false`). |
| options.progressive<span class="arg-type">boolean</span> | If exporting a JPG, export a progressive JPEG (only used when exporting to `jpeg`) (defaults to `false`). |
| options.compression<span class="arg-type">number</span> | If exporting a JPG, the compression level to use fo `jpeg` (with `0` being the completely compressed, `1.0` no compression) (defaults to `1.0`). |