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.
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.
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
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