Batch convert .graffle files

Is there a way to batch convert a folder of several hundred .graffle files to another format, probably jpg, but perhaps png or svg?

It would be a huge timesaver over opening and exporting each one.

Hey, ThosWolfe. I’m afraid there isn’t a batch conversion tool right now, but that sounds like a great suggestion to pass on to our Support team so we can file your +1 in our open feature request for that. If you could drop the team a line at omnigraffle@omnigroup.com, we’ll file your support right away!

While our Support Team doesn’t write and support custom AppleScripts, this sounds like a workflow that a script could help with. If you were interested in exploring that a little further the OmniGraffle automation forum would be a really good place to start: https://discourse-test.omnigroup.com/c/omnigraffle/omnigraffle-automation

Hi Austin,

Does OG7 omniJS (7.4 test (v179.5 r289738)) allow automated file exports ?

In the omniJS console we can list UTIs by typing:

document.supportedExportTypes().join('\n')

but I may well be missing something – I can’t see a method for exporting/saving in a chosen format …

Please advise

This isn’t currently possible in OmniJS, as we don’t yet have full support for attaching scripts to documents to be automatically performed when certain actions are executed. I’ll let the team know you’d like to see this added as OmniJS work continues!

In lieu of ‘automated’, perhaps I should have said ‘scripted’ – I wasn’t thinking about automatic triggering – simply of running a script (e.g. through a url) to export a file.

But perhaps there is a slight tension between the use of a JS Context and direct access to the file system ?

Perhaps the ideal pattern would be to delegate file system access to (JXA) JavaScript for Automation ?

PS the key thing I am waiting for is getting a return value from omniJS (to JS script submitted for evaluation to omniJS by JXA/AppleScript, or iOS Workflow etc, as in the TaskPaper.evaluateScript method)

That will really unlock productive use of omniJS for me.

Not sure what workflow you have in mind, e.g.

  • Dialog/script settings to choose source files/folder, or just files/folder currently selected in Finder
  • ditto to choose output format, or just default output format
  • ditto to choose output file folder and names, or just some defaults

etc …

But in the meanwhile, here are a few JavaScript for Automation functions from which such a thing could be built:

(Note, these functions are in ES6 format which works with Sierra onwards. ES6 to ES5 conversion for Yosemite onwards can be obtained by pasting the code into the repl at https://babeljs.io/repl)

(() => {

    // MACOS FILE SYSTEM FUNCTIONS -------------------------------------------

    // doesFileExist :: String -> Bool
    const doesFileExist = strPath => {
        var error = $();
        return (
            $.NSFileManager.defaultManager
            .attributesOfItemAtPathError(
                $(strPath)
                .stringByStandardizingPath,
                error
            ),
            error.code === undefined
        );
    };

    // doesDirectoryExist :: String -> IO Bool
    const doesDirectoryExist = strPath => {
        const
            dm = $.NSFileManager.defaultManager,
            ref = Ref();
        return dm
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && ref[0] === 1;
    };

    // for type strings see: Apple's 'System-Declared Uniform Type Identifiers'
    // if strType is omitted, files of all types will be selectable
    // String -> String -> String
    const pathChoice = (strPrompt, strType) => {
        const a = Application.currentApplication();
        return (a.includeStandardAdditions = true, a)
            .chooseFile({
                withPrompt: strPrompt,
                ofType: strType
            })
            .toString();
    };

    // selectedPaths :: () -> [pathString]
    const selectedPaths = () =>
        Application('Finder')
        .selection()
        .map(x => decodeURI(x.url())
            .slice(7));

    // standardPath :: String -> Path
    const standardPath = strPath =>
        Path(ObjC.unwrap($(strPath)
            .stringByStandardizingPath));

    // pathFolderExists :: strPath -> Bool
    const pathFolderExists = strPath =>
        doesDirectoryExist(
            ObjC.unwrap($(strPath)
                .stringByDeletingLastPathComponent)
        );

    // GENERIC FUNCTIONS ----------------------------------------------------

    // compare :: a -> a -> Ordering
    const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);

    // curry :: ((a, b) -> c) -> a -> b -> c
    const curry = f => a => b => f(a, b);

    // elem :: Eq a => a -> [a] -> Bool
    const elem = (x, xs) => xs.indexOf(x) !== -1;

    // findIndex :: (a -> Bool) -> [a] -> Maybe Int
    const findIndex = (p, xs) =>
        xs.reduce((a, x, i) =>
            a.nothing ? (
                p(x) ? {
                    just: i,
                    nothing: false
                } : a
            ) : a, {
                nothing: true
            });

    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = f => (a, b) => f.apply(null, [b, a]);

    // foldl :: (b -> a -> b) -> b -> [a] -> b
    const foldl = (f, a, xs) => xs.reduce(f, a);

    // isPrefixOf :: [a] -> [a] -> Bool
    const isPrefixOf = (xs, ys) => {
        const [_xs, _ys] = typeof xs === 'string' ? (
            [xs.split(''), ys.split('')]
        ) : [xs, ys];
        return xs.length ? (
            ys.length ? _xs[0] === _ys[0] && isPrefixOf(
                _xs.slice(1), _ys.slice(1)
            ) : false
        ) : true;
    };

    // last :: [a] -> a
    const last = xs => xs.length ? xs.slice(-1)[0] : undefined;

    // length :: [a] -> Int
    const length = xs => xs.length;

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // For n-ary sorts:
    // derives a comparator function from a list of property-getting functions
    // mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
    const mappendComparing = fs => (x, y) =>
        fs.reduce((ord, f) => (ord !== 0) ? (
            ord
        ) : (() => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : a > b ? 1 : 0
        })(), 0);

    // isNull :: [a] | String -> Bool
    const isNull = xs =>
        Array.isArray(xs) || typeof xs === 'string' ? (
            xs.length < 1
        ) : undefined;

    // log :: a -> IO ()
    const log = (...args) =>
        console.log(
            args
            .map(show)
            .join(' -> ')
        );

    // show :: a -> String
    const show = (...x) =>
        JSON.stringify.apply(
            null, x.length > 1 ? [x[0], null, x[1]] : x
        );

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = (f, xs) =>
        xs.slice()
        .sort(f);

    // splitOn :: String -> String -> [String]
    const splitOn = (cs, xs) => xs.split(cs);

    // stripPrefix :: Eq a => [a] -> [a] -> Maybe [a]
    const stripPrefix = (p, s) => {
        const
            blnString = typeof p === 'string',
            [xs, ys] = blnString ? (
                [p.split(''), s.split('')]
            ) : [p, s];
        const
            sp_ = (xs, ys) => xs.length === 0 ? ({
                just: blnString ? ys.join('') : ys,
                nothing: false
            }) : (ys.length === 0 || xs[0] !== ys[0]) ? {
                nothing: true
            } : sp_(xs.slice(1), ys.slice(1));
        return sp_(xs, ys);
    };

    // toLower :: Text -> Text
    const toLower = s => s.toLowerCase();

    // testWriter :: (a -> Bool) -> String -> (a -> {ok: Bool, error: String})
    const testWriter = (p, s) => x => {
        const isOK = p(x);
        return {
            ok: isOK,
            error: isOK ? '' : (show(x) + ': ' + s)
        };
    };

    // testResults :: [(a -> Bool, a)] -> {ok: Bool, error: string}
    const testResults = pxs =>
        foldl((a, [p, x]) => {
            const m = p(x);
            return {
                ok: a.ok ? m.ok : false,
                error: isNull(m.error) ? (
                    a.error
                ) : a.error + m.error + '\n'
            }
        }, {
            ok: true,
            error: ''
        }, pxs);

    // OMNIGRAFFLE FUNCTIONS -------------------------------------------------

    // maybeOgDocExported :: String -> String -> String
    //                            -> {Nothing: Bool, Just: String}
    const maybeOgDocExported = (inPath, outUTI, outPath) => {
        const dctTests = testResults([
            [twFileExists, inPath],
            [twUTIRecognized, outUTI],
            [twOutputFolderExists, outPath]
        ]);
        return dctTests.ok ? (() => {
            const
                d = Application('OmniGraffle').open(standardPath(inPath));
            return d ? (
                d.save({
                    as: outUTI,
                    in: standardPath(outPath)
                }),
                d.close(), {
                    nothing: !doesFileExist(outPath),
                    just: outPath
                }) : {
                nothing : true,
                error: inPath + ' was not successfully opened'
            };
        })() : {
            nothing: true,
            error: dctTests.error
        };
    };

    // exportableUTIs :: [String]
    const exportableUTIs = [
        'com.omnigroup.omnigraffle.graffle' //
        , 'com.omnigroup.omnigraffle.graffle-package' //
        , 'com.adobe.pdf' //
        , 'public.tiff' //
        , 'public.png' //
        , 'com.compuserve.gif' //
        , 'public.jpeg' //
        , 'public.svg-image' //
        , 'com.adobe.encapsulated-postscript' //
        , 'com.omnigroup.omnigraffle.HTMLExport' //
        , 'com.omnigroup.omnioutliner.oo3' //
        , 'com.microsoft.bmp' //
        , 'com.omnigroup.foreign-types.ms-visio.xml' //
        , 'com.adobe.photoshop-image' //
        , 'com.omnigroup.omnigraffle.diagramstyle' //
        , 'com.omnigroup.omnigraffle.diagramstyle-package' //
        , 'com.omnigroup.omnigraffle.template' //
        , 'com.omnigroup.omnigraffle.template-package' //
        , 'com.omnigroup.omnigraffle.gstencil' //
        , 'com.omnigroup.omnigraffle.gstencil-package'
    ];

    // utiRecognized :: String -> Bool
    const utiRecognized = s => elem(s, exportableUTIs);

    // utiAbbrevn :: String -> String
    const utiAbbrevn = s =>
        elem('svg', s) ? 'svg' : last(splitOn('.', s));

    // utiFromAbbrevn :: String -> String
    const utiFromAbbrevn = s => {
        const mbUTI = findIndex(curry(elem)(s), exportableUTIs);
        return mbUTI.nothing ? '' : exportableUTIs[mbUTI.just];
    };

    const
        twFileExists = testWriter(doesFileExist, "nothing found at this path"),
        twUTIRecognized = testWriter(utiRecognized, "UTI not recognized"),
        twOutputFolderExists = testWriter(
            pathFolderExists, "output folder does not exist"
        );

    // utiMenu :: [String]
    // const utiMenu = map(utiAbbrevn, sortBy(flip(compare), exportableUTIs));
    // OR
    const utiMenu = sortBy(
        mappendComparing([length, toLower]),
        map(utiAbbrevn, exportableUTIs)
    );

    // TEST ------------------------------------------------------------------

    return show(
        maybeOgDocExported(
            '~/Desktop/sample.graffle', 'public.png', '~/Desktop/test006.png'
        )
    );
    // Returns a dictionary with two keys: 'nothing' and 'just'

    // Nothing will be set to True if something failed (file not found etc)

    // If 'nothing' is False, 'just' can be read for the resulting output path.

    // -> {"nothing":false,"just":"~/Desktop/test006.png"}

    // -----------------------------------------------------------------------
    // Available export format UTI strings for argument 2 in maybeOgDocExported:
})();

Here’s a script which prompts for a folder, then converts every OmniGraffle document in that folder to PNG files. (It could easily be modified to export other formats, just search for the reference to “png”.)

-- First, prompt for the folder containing the documents you want to export and find all the *.graffle files in that folder
set folderName to quoted form of POSIX path of (choose folder)
set findResults to (do shell script "find " & folderName & " -name '*.graffle' | sed 's#//#/#'")
set graffleFiles to every text item of splitString(findResults, return)

-- Now iterate through all those files and convert them
repeat with graffleFile in graffleFiles
	convertGraffleFile(graffleFile)
end repeat

-- Here is the function which converts a single document. It exports each document as a folder which contains a separate PNG file for each canvas. (If you want something different, replace "entire document" with "current canvas" or whatever.)
on convertGraffleFile(graffleFile)
	log "Converting " & graffleFile
	tell application "OmniGraffle"
		set area type of current export settings to entire document
		open graffleFile
		set targetFile to graffleFile & ".png"
		log "... saving " & targetFile
		tell front document to save in POSIX file targetFile
		close front document
	end tell
end convertGraffleFile

-- This is just a little utility function to split the lines of output returned by the UNIX find command
on splitString(theString, theDelimiter)
	set oldDelimiters to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	set theArray to every text item of theString
	set AppleScript's text item delimiters to oldDelimiters
	return theArray
end splitString
2 Likes

Hi Ken. I’ve been researching this requirement to batch convert all Omnigraffle files to Visio exports and this certainly works. I love your work.

What I want to do though is have this script automatically run via Hazel so I don’t have to manually run the script and ask to specify the folder every time. If Hazel finds a new OG file in a folder, I want it to run the script and create a Visio version in the same location.

I’m just unsure of how to alter the start of this script so it doesn’t need to ask for the folder but instead just uses the current location of where the file is being run from.

I’m sure you can do that in your sleep but as a non-applescripts guy, I’m all at sea. Would you mind just nudging me in the right direction here as to what needs to change please?

Thanks in advance for your help Ken.

Cheers
Dean