Hi @Abpujar, I have good news I think. I've been working on a script that will "unembed" raster items. Maybe this is what you need? Currently the script will "unembed" all raster items in document. It will save them into a "Links" folder in the same folder as the .ai document as .psd files and link them (so they will be linked placed items). I couldn't find any other scripts that solved this problem. Let me know if it helps.
- Mark
/**
* Unembed Raster Items
* for Adobe Illustrator 2022
* @author m1b
* @discussion https://community.adobe.com/t5/illustrator-discussions/is-it-possible-to-convert-rasteritem-to-placeditem/m-p/13081172
*
* Known issues:
* 1. raster items with effects applied
* will be rasterized and the effect will
* become part of the linked image;
* 2. the resolution of the effect will match
* the resolution of the embedded image, not
* the document raster resolution setting,
* and sometimes the effect scale will be wrong;
* 3. some effects will cause the linked image
* to be offset in position compared to the
* embedded image.
* 4. the script uses various sources to derive
* the unembedded file's name, including the
* file's XMP manifest, and this hasn't been
* tested on a wide range of documents.
*/
(function () {
// Will use the group name if it contains a file extension
// (I recommend leaving this on, unless it causes a problem
// in your case.)
var useGroupNameAsFileName = true;
// if testMode == true, don't close the temp document
var testMode = false;
if (app.documents.length > 0)
unembedRasterImages(app.activeDocument, useGroupNameAsFileName);
/**
* Unembeds all rasterImages in document.
* @author m1b
* @version 2023-01-17
* It will export embedding images as PSD
* format into a 'Links' folder in the
* same folder as the Illustrator document.
* It will add suffix to exported images
* to ensure it does NOT overwrite files
* that already exist in the Links folder.
* @param {Document} doc - an Illustrator document.
* @param {Boolean} useGroupNameAsFileName - whether to Will use the group name if it contains a file extension (default: true).
*/
function unembedRasterImages(doc, useGroupNameAsFileName) {
var previousInteractionLevel = app.userInteractionLevel;
app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS;
var myImages = doc.rasterItems,
imageCount = myImages.length,
removeMeUUIDs = [],
exportErrors = [],
unembeds = [],
counter = 0;
// set up export folder
var exportFolder = Folder(doc.fullName.parent.fsName + '/Links/');
if (!exportFolder.exists)
exportFolder.create();
// set up the images for export
for (var i = imageCount - 1; i >= 0; i--) {
var oldImage = myImages[i],
colorSpace = oldImage.imageColorSpace,
position = oldImage.position,
// get current scale and rotation
sr = getLinkScaleAndRotation(oldImage),
scale = [sr[0], sr[1]],
rotation = sr[2],
width = oldImage.width,
height = oldImage.height,
fileExtensionRegex = /\.[^\.]+$/,
imageTitle = oldImage.name.replace(fileExtensionRegex, '') || undefined;
try {
imageTitle = (decodeURIComponent(oldImage.file.name)).replace(fileExtensionRegex, '');
} catch (error) { }
if (
useGroupNameAsFileName !== false
&& oldImage.parent.name.match(fileExtensionRegex)
)
imageTitle = oldImage.parent.name.replace(fileExtensionRegex, '');
var workingTitle = imageTitle || 'image' + (i + 1);
// script can't handle other colorSpaces
if (
colorSpace != ImageColorSpace.CMYK
&& colorSpace != ImageColorSpace.RGB
&& colorSpace != ImageColorSpace.GrayScale
) {
exportErrors.push(workingTitle + ' has unsupported color space. (' + colorSpace + ')');
continue;
}
// move to new document for exporting
var temp = newDocument(workingTitle, colorSpace, 1000, 1000);
// duplciate to the new document
var workingImage = oldImage.duplicate(temp.layers[0], ElementPlacement.PLACEATBEGINNING);
// set image to 100% scale 0° rotation and position
var tm = app.getRotationMatrix(-rotation);
tm = app.concatenateScaleMatrix(tm, 100 / scale[0] * 100, 100 / scale[1] * 100);
workingImage.transform(tm, true, true, true, true, true);
workingImage.position = [0, workingImage.height];
temp.artboards[0].artboardRect = [0, workingImage.height, workingImage.width, 0];
// export
try {
var workingFileName = new Date().getTime(),
path = exportFolder.fsName + '/' + workingFileName + '.psd',
// path = getFilePathWithOverwriteProtectionSuffix(exportFolder.fsName + '/' + workingFileName + '.psd'),
workingFile = exportAsPSD(temp, path, colorSpace, 72);
} catch (error) {
exportErrors.push('"' + imageTitle + '" failed to export. (' + error.message + ')');
}
if (!testMode)
// close the temp document
temp.close(SaveOptions.DONOTSAVECHANGES);
if (
workingFile == undefined
|| !workingFile.exists
)
continue;
// see if the exported file has fileName in it's XMP manifest
// var xmpFileName = getFilePathFromXMP(file, true);
imageTitle = (
imageTitle
|| getFilePathFromXMP(workingFile, true)
|| 'image' + (i + 1)
);
// rename the working file
var newPath = exportFolder.fsName + '/' + imageTitle + '.psd';
workingFile.copy(newPath);
var file = File(newPath);
workingFile.remove();
// relink to the embedded image
var newImage = oldImage.layer.placedItems.add();
newImage.file = file;
// scale and rotate and position
// to match the original
tm = app.getScaleMatrix(scale[0], scale[1]);
tm = app.concatenateRotationMatrix(tm, rotation);
newImage.transform(tm, true, true, true, true, true);
newImage.move(oldImage, ElementPlacement.PLACEAFTER);
newImage.position = position;
if (
round(oldImage.width, 2) != round(newImage.width, 2)
|| round(oldImage.height, 2) != round(newImage.height, 2)
) {
// When size is different here, it probably means
// there was an effect on the rasterItem eg.dropshadow.
// Change `false` to `true` below if you want script
// to ignore rasterItems that have this problem.
if (false) {
exportErrors.push('Did not unembed "' + imageTitle + '" because it couldn\'t be sized correctly. Try removing special appearance and trying again.')
file.remove();
newImage.remove();
continue;
}
else
exportErrors.push('Warning: File "' + imageTitle + '" has altered dimensions, probably due to an effect or special appearance.');
}
// delete old image later
removeMeUUIDs.push(oldImage.uuid);
// for reporting
counter++
}
// remove old embedded images
for (var i = 0; i < removeMeUUIDs.length; i++) {
var removeMe = getItemByUUID(myImages, removeMeUUIDs[i]);
if (removeMe != undefined)
removeMe.remove();
}
// clean up
app.userInteractionLevel = previousInteractionLevel;
app.redraw();
// reporting
var result = 'Unembedded ' + counter + ' of ' + imageCount + ' raster items.';
if (exportErrors.length > 0)
result += '\n' + exportErrors.join('\n');
alert(result);
};
/**
* Returns a page item with matching UUID.
* @param {PageItems} items - Illustrator PageItems or Array of PageItems.
* @param {Number} uuid - Illustrator PageItem uuid.
* @returns {PageItem}
*/
function getItemByUUID(items, uuid) {
for (var i = 0; i < items.length; i++) {
if (items[i].uuid === uuid)
return items[i];
}
};
/**
* Exports a document as PSD.
* @param {Document} doc - the document to export.
* @param {String} path - name of file, without extension.
* @param {ImageColorSpace} colorSpace - the colorSpace of the image.
* @param {Number} resolution - export resolution
* @returns {File} - the exported psd file.
*/
function exportAsPSD(doc, path, colorSpace, resolution) {
var file = File(path),
options = new ExportOptionsPhotoshop();
options.antiAliasing = false;
options.artBoardClipping = true;
options.imageColorSpace = colorSpace;
options.editableText = false;
options.flatten = true;
options.maximumEditability = false;
options.resolution = (resolution || 72);
options.warnings = false;
options.writeLayers = false;
doc.exportFile(file, ExportType.PHOTOSHOP, options);
return file;
};
/**
* Return the scale, rotation and size
* of a PlacedItem or RasterItem.
* @author m1b
* @version 2023-03-09
* @param {PlacedItem|RasterItem} item - an Illustrator item.
* @param {Boolean} round - whether to round numbers to nearest integer.
* @returns {Array} [scaleX%, scaleY%, rotation°, width, height]
*/
function getLinkScaleAndRotation(item, round) {
if (item == undefined)
return;
var m = item.matrix,
rotatedAmount,
unrotatedMatrix,
scaledAmount;
var flipPlacedItem = (item.typename == 'PlacedItem') ? 1 : -1;
try {
rotatedAmount = item.tags.getByName('BBAccumRotation').value * 180 / Math.PI;
} catch (error) {
rotatedAmount = 0;
}
unrotatedMatrix = app.concatenateRotationMatrix(m, rotatedAmount * flipPlacedItem);
if (
unrotatedMatrix.mValueA == 0
&& unrotatedMatrix.mValueB !== 0
&& unrotatedMatrix.mValueC !== 0
&& unrotatedMatrix.mValueD == 0
)
scaledAmount = [unrotatedMatrix.mValueB * 100, unrotatedMatrix.mValueC * -100 * flipPlacedItem];
else
scaledAmount = [unrotatedMatrix.mValueA * 100, unrotatedMatrix.mValueD * -100 * flipPlacedItem];
if (scaledAmount[0] == 0 || scaledAmount[1] == 0)
return;
if (round)
return [round(scaledAmount[0]), round(scaledAmount[1]), round(rotatedAmount)];
else
return [scaledAmount[0], scaledAmount[1], rotatedAmount];
};
/**
* Rounds `n` to `places` decimal places.
* @param {Number} n - the number to round
* @param {Number} places - number of decimal places, can be negative
* @returns {Number}
*/
function round(n, places) {
var m = Math.pow(10, places != undefined ? places : 3);
return Math.round(n * m) / m;
};
/**
* Create a new basic document with some options.
* @author m1b
* @version 2022-07-21
* @param {String} name - the title of the document.
* @param {ImageColorSpace} colorSpace - ImageColorSpace.RGB or ImageColorSpace.CMYK.
* @param {Number} width - width of the default artboard.
* @param {Number} height - height of the default artboard.
* @returns {Document}
*/
function newDocument(name, colorSpace, width, height) {
var myDocPreset = new DocumentPreset(),
myDocPresetType;
myDocPreset.title = name;
myDocPreset.width = width || 1000;
myDocPreset.height = height || 1000;
if (
colorSpace == ImageColorSpace.CMYK
|| colorSpace == ImageColorSpace.GrayScale
|| colorSpace == ImageColorSpace.DeviceN
) {
myDocPresetType = DocumentPresetType.BasicCMYK;
myDocPreset.colorMode = DocumentColorSpace.CMYK
}
else { // if (colorSpace == ImageColorSpace.RGB) {
myDocPresetType = DocumentPresetType.BasicRGB;
myDocPreset.colorMode = DocumentColorSpace.RGB
}
return app.documents.addDocument(myDocPresetType, myDocPreset);
};
/**
* Returns a path that will not overwrite a file.
* Adds an incremental suffix to avoid overwrites.
* @author m1b
* @version 2022-07-20
* For example, if `myFile.txt` already exists it
* will return `myFile(1).txt` and if that already
* exists, it will return `myFile(2).txt` etc.
* @param {String} path - the path to test.
* @returns {String} a path that will not overwrite a file.
*/
function getFilePathWithOverwriteProtectionSuffix(path) {
var index = 1,
parts = path.split(/(\.[^\.]+)$/);
while (File(path).exists)
path = parts[0] + '(' + (++index) + ')' + parts[1];
return path;
};
/**
* Returns the file path stored in XMP data
* of a psd file, if available.
* @param {File} file - a File.
* @param {Boolean} fileNameOnly - whether to return just file names (default: false).
* @returns {String} - file path or file name or undefined.
*/
function getFilePathFromXMP(file, fileNameOnly) {
if (ExternalObject.AdobeXMPScript == undefined)
ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
var xmpFile = new XMPFile(file.fsName, XMPConst.UNKNOWN, XMPConst.OPEN_FOR_READ),
xmp = xmpFile.getXMP(),
xpath = 'xmpMM:Manifest[1]/stMfs:reference/stRef:filePath',
value = xmp.getProperty(XMPConst.NS_XMP_MM, xpath);
if (
value == undefined
|| !value.hasOwnProperty('value')
)
return;
value = value.value;
if (fileNameOnly !== false) {
var onlyFileName = value.match(/\/([^\/]+)\.[^\.\/]+/);
if (
onlyFileName != null
&& onlyFileName.length == 2
)
value = onlyFileName[1];
}
return value;
};
})();
Edit 1: fixed un-localized string in newDocument function. Thanks @Kurt Gold!
Edit 2: fixed problem with rotated rasterItems and now shows warning in special cases. Again thanks @Kurt Gold!
Edit 3 (2022-12-03): added better file naming, as requested by @ignacio267368455fix. Good call!
Edit 4 (2022-12-03): @ignacio267368455fix found cases where the RasterItem's name was actually in the parent GroupItem. I've added an option "useGroupNameAsFileName" to handle this case.
Edit 5 (2023-01-15): added ability to use fileNames taken from the document's XMP manifest, to handle a case in @Filip.Czechowski's sample document. This feature could do with some more testing, but worked well for Filip's file.
Edit (2023-01-16): added a step to save the document, to ensure that the XMP manifest was updated because after reordering raster items in the document (without saving) the manifest would have the old ordering.
Edit (2023-01-17): my previous attempt to get meaningful file names using the document's XMP data didn't pan out in many cases, so I implemented an idea by @Filip.Czechowski to get the filename from the exported document's XMP data.
Edit (2023-03-09): Added a check for an edge case, found by @ajabon grinsmith, where the raster item had no BBAccumRotation tag and returned zero scale.