I have an SVG object that looks like this:
Each of the inner <g> elements have <path>s in them.
I want to export this SVG to PDF so the groups translate to layers (OCGs), like this:
<ThroughCut>
<Path>
<Path>
<Path>
<Path>
<Graphics>
<Path>
<Path>
<Path>
<Path>
Yet any tool I have tried for this puts all objects in the same layer, and basically throws away information about groups.
Solutions in JavaScript or Python are preferred, but anything that executes from the command line on a UNIX machine will do.
I solved my problem as stated here, by following this PyMuPDF issue on Github.
Since I have control over the input SVG, I managed to solve the problem by parsing two SVGs to PDFs and combining them in separate layers of a new document. This is what I'm doing:
import fitz
from svglib.svglib import svg2rlg
from reportlab.graphics.renderPDF import drawToString
def svg_to_doc(path):
"""Using this function rather than `fitz`' `convertToPDF` because the latter
fills every shape with black for some reason.
"""
drawing = svg2rlg(path)
pdfbytes = drawToString(drawing)
return fitz.open("pdf", pdfbytes)
# Create a new blank document
doc = fitz.open()
page = doc.new_page()
# Create "Layer1" and "Layer2" OCGs and get their `xref`s
xref_1 = doc.add_ocg('Layer1', on=True)
xref_2 = doc.add_ocg('Layer2', on=True)
# Load "layer_1" and "layer_2" svgs and convert to pdf
doc_1 = svg_to_doc("my_layer_1.svg")
doc_2 = svg_to_doc("my_layer_2.svg")
# Set the `page` dimensions. Note: for me it makes sense to set the bounding
# box of the output to the same as `doc_1`, because I know `doc_1` contains
# `doc_2`. If that were not the case, I would set `bb` to be a new
# `fits.Rect` object that contained both `doc_1` and `doc_2`.
bb = doc_1[0].rect
page.setMediaBox(bb)
# Put the docs in their respective OCGs
page.show_pdf_page(bb, doc_1, 0, oc=xref_1)
page.show_pdf_page(bb, doc_2, 0, oc=xref_2)
# Save
doc.save("output.pdf")
If I load "output.pdf" in Adobe Acrobat the layers show. Curiously, the same is not the case for Adobe Illustrator (here they are simply "Clip Groups"). Regardless, I believe this solves the problem as stated above.
my_layer_1.svg
my_layer_2.svg
My other solution, although correct, does not produce a PDF that is compatible with Adobe standards (for example, Illustrator will not see the OCGs as legitimate layers—although strangely, Acrobat will).
In case one needs to produce a PDF that is compatible with Adobe standards, and will be loaded correctly in Illustrator, another option is to use the Illustrator scripting API.
Here's a script that one can use to convert a loaded SVG file into a PDF with the desired layer structure.
/** Convert an open file in Illustrator to PDF, after removing the first layer.
* Useful for converting SVGs into PDFs, where it is desired that the first level
* `<g>` elements are converted to layers/OCGs in the exported PDF.
*/
// Select export destination
const destFolder = Folder.selectDialog( 'Select folder for PDF files.', '~' );
// Get the PDF options to be used
const options = getOptions();
// The SVG should have a single `Layer1` top layer...
const doc = app.activeDocument;
if (doc.layers.length == 1) {
// ... remove it
removeFirstLayer(doc)
// Create a file pointer for export...
var targetFile = getTargetFile(doc.name, '.pdf', destFolder);
// ... and save save `doc` in the file pointer.
doc.saveAs(targetFile, options);
}
/* --------- */
/* Utilities */
/* --------- */
function getOptions() {
// Create PDFSaveOptions object
var pdfSaveOpts = new PDFSaveOptions();
// Set PDFSaveOptions properties (toggle these comment/uncomment)
pdfSaveOpts.acrobatLayers = true;
pdfSaveOpts.colorBars = true;
pdfSaveOpts.colorCompression = CompressionQuality.AUTOMATICJPEGHIGH;
pdfSaveOpts.compressArt = true; //default
pdfSaveOpts.embedICCProfile = true;
pdfSaveOpts.enablePlainText = true;
pdfSaveOpts.generateThumbnails = true; // default
pdfSaveOpts.optimization = true;
pdfSaveOpts.pageInformation = true;
// pdfSaveOpts.viewAfterSaving = true;
return pdfSaveOpts;
}
function removeFirstLayer(doc) {
// Get the layer to be removed
var firstLayer = doc.layers[0];
// Convert groups into new layers
for (var i=firstLayer.groupItems.length-1; i>=0; i--) {
var group = firstLayer.groupItems[i];
var newLayer = firstLayer.layers.add();
newLayer.name = group.name;
for (var j=group.pageItems.length-1; j>=0; j--)
group.pageItems[j].move(newLayer, ElementPlacement.PLACEATBEGINNING);
}
// Move new layers to the document and remove `firstLayer`
for (var i=firstLayer.layers.length-1; i>=0; i--)
firstLayer.layers[i].move(firstLayer.parent, ElementPlacement.PLACEATBEGINNING);
firstLayer.remove();
}
function getTargetFile(docName, ext, destFolder) {
var newName = "";
// Add extension is none exists
if (docName.indexOf('.') < 0)
newName = docName + ext;
else
newName += docName.substring(0, docName.lastIndexOf('.')) + ext;
// Create file pointer
var myFile = new File(destFolder + '/' + newName);
// Check that file permissions are granted
if (myFile.open("w"))
myFile.close();
else
throw new Error('Access is denied');
return myFile;
}
Put the script in a file that ends with .jsx, and place in inside your Scripts folder in Illustrator. Mine is located at /Applications/Adobe Illustrator 2021/Presets/en_US/Scripts/svgToPDF.jsx.
Restart Illustrator
Execute the script from the File > Scripts > svgToPDF menu item.
Drawbacks
Illustrator costs money.
You have to manually execute the SVG -> PDF conversion. Could be automated with AppleScript, but it's really not something you want to have running on a server.
Related
In my previous post1 and post2, i managed to fix the choropleth map/legend issue + draw circles problems when drawing a map.
When i follow this must-do tutorial about choropleth and when i search on internet i always find the same logic
d3.csv("my.csv", function(data) {
d3.json(myjson, function(json) {
for (var i = 0; i < data.length ; i++) {
//Grab state name
var dataState = data[i].nom;
//Grab data value, and convert from string to float
var dataValue = data[i].population;
//Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.nom;
if (dataState == jsonState) {
//Copy the data value into the JSON
json.features[j].properties.CA = dataValue;
//Stop looking through the JSON
break;
}
}
}
So in my case, i have a map with 75 path (1 path=region) and my csv file have 75 rows (1 row = 1 path)
Now i'm trying to do things a little differently
My new csv has N rows (N > 75, let's say 200) and for each row, a store (properties+lat+lon) is affected to a path ==> i can have 5 stores/path e.g
Here are my questions :
1) How do i write my choropleth code differently ==> I'd like to scan the csv file and return for each distinct path the sum of specific properties (here "income") in order to write it on my json file ???
2) When i click on a specific region/path, i'd like to display on a new div (in my case #output) the json file corresponding to my region (basicaly i have 75 json files "region1.json", "region2.json" and so on...") with circles inside (one circle = one store, in my csv file "name" column") ==> How do i retrieve this "on click value" and call the correct/corresponding json file ????
3) Finally, if i click on a displayed specific circle of the #output div, i'd like to have on a third div a chart ==> How do i writte correctly my 3rd div so it is correctly displayed (css, other ?? ==> it can be applied to #output too) ??
Thank you so much for reading this request and for your availability and help
Here's the plunker file (do not mind the sales.csv file, i just used it to try displaying something when i click on the path
Thanks again
d3 nest roll up is the solution
d3.csv("source-data.csv", function(error, csv_data) {
var data = d3.nest()
.key(function(d) { return d.date;})
.rollup(function(d) {
return d3.sum(d, function(g) {return g.value; });
}).entries(csv_data);
});
More info here
I'm using my own custom connector implementation and i want to be able to consider other connectors to the same elements in order to calculate the source and target points better.
joint.connectors.custom = function (sourcePoint, targetPoint, vertices) {
// I want to calculate the "middle" point while considering other links that might interrupt
var sourceMiddleX = this.sourceBBox.x + this.sourceBBox.width / 2;
var d = ['M', sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y];
return d.join(' ');
};
So far i couldn't find anything helpful under the function context nor under the VElement..
Unless anyone has a better idea, i'll pass the total links per element in each model which doesn't feels right.
Thanks in advance!
Connector functions in JointJS are called with a value of this equal to a joint.dia.LinkView instance. Each linkView has access to the paper it's rendered in.
var paper = this.paper; // an instance of `joint.dia.Paper`
var graph = this.paper.model; // an instance of `joint.dia.Graph`
var link = this.model; // an instance of `joint.dia.Link`
// an instance of `joint.dia.Element` or `null`
var sourceElement = link.getSourceElement();
var sourceElementLinks = sourceElement
// an array of `joint.dia.Link` including the link
// these are connecting the same source element as the link
? graph.getConnectedLinks(sourceElement, { outbound: true })
// the link is not connected to a source element
: [];
// an instance of `joint.dia.Element` or `null`
var targetElement = link.getTargetElement();
var targetElementLinks = targetElement
// an array of `joint.dia.Link` including the link
// these are connecting the same target element as the link
? graph.getConnectedLinks(targetElement, { inbound: true })
// the link is not connected to a target element
: [];
You can also check the jumpOver connector, that implements a similar logic.
I've been reading this article: http://blogs.adobe.com/cantrell/archives/2011/03/native-cursors-in-air-2-6.html on how to create a native cursor in AIR without having to hack it by moving a sprite in place of a hidden cursor to fake it.
However I'm using HTML/JavaScript instead of ActionScript.
So far I have:
function nativeCursor(){
// load in a bitmap
var loader = new air.Loader();
loader.load(new air.URLRequest('./assets/cursor.png'));
var bitmaps = new air.Vector["<String>"]();
var bmd = new air.BitmapData(32, 32, true, 0x00000000);
var p = new window.runtime.flash.geom.Point(0, 0);
var r = new window.runtime.flash.geom.Rectangle(32 , 0, 32, 32);
var image = new window.runtime.flash.display.Bitmap(loader.content);
bmd.copyPixels([image.bitmapData], r, p);
bitmaps.push(bmd);
var mcd = new window.runtime.flash.ui.MouseCursorData();
mcd.data = bitmaps;
mcd.hotSpot = new Point(0, 0);
mcd.frameRate = 24;
window.runtime.flash.ui.Mouse.registerCursor("defaultCursor", mcd);
window.runtime.flash.ui.Mouse.cursor = "defaultCursor";
}
But I get an error TypeError: Error #1034: Type Coercion failed: cannot convert []#2b9d1f1 to flash.display.BitmapData. for this line: bmd.copyPixels([image.bitmapData], r, p);
If I remove the brackets for that line so it's just: bmd.copyPixels(image.bitmapData, r, p); the error becomes TypeError: Error #2007: Parameter sourceBitmapData must be non-null.
So I'm assuming that the error is because the bitmap data is null... but why? The image is being loaded in fine so is the way I'm trying to get the bitmap data incorrect?
BitmapData, not String
The vector is supposed to be of type BitmapData, not String, that is:
air.Vector["<flash.display.BitmapData>"]
See HTML Developer’s Guide for Adobe AIR - Working with Vectors for more information.
Asynchronous loaders
Also the Loader class probably works asynchronously in JavaScript too, it's not documented properly in the HTML API reference and I've never used JS for AIR development, so I can only assume that, and that one can refer to the AS3 reference for the missing docs, however it makes sense judging from the available examples.
http://help.adobe.com/.../html/flash/display/BitmapData.html#includeExamplesSummary
Loader.bitmapData doesn't exist
There is no bitmapData property on the Loader class, only a content property that holds a DisplayObject, which might actually be a Bitmap object which in turn has a bitmapData property.
An example
Here's some untested example code that should get you started:
var mcd = new window.runtime.flash.ui.MouseCursorData();
mcd.hotSpot = new air.Point(0, 0);
mcd.frameRate = 24;
var loader = new air.Loader();
loader.contentLoaderInfo.addEventListener(air.Event.COMPLETE, function(event)
{
var image = air.Bitmap(loader.content);
var bitmaps = new air.Vector["<flash.display.BitmapData>"]();
bitmaps.push(image.bitmapData);
mcd.data = bitmaps;
air.Mouse.registerCursor('defaultCursor', mcd);
air.Mouse.cursor = 'defaultCursor';
});
var request = new air.URLRequest('./assets/cursor.png');
loader.load(request);
I am trying to run an animation from a JSON file. I am using a custom JSON loader, (i.e. not the one included with three.js).
So I have an object named frames, which contain many frames, all of them have shape information, and a simulation_matrix, which contains data required for animation in the form of a 4by4 transformation matrix(generated from a python script).
So I am using this code for animation ..
and this is a sample JSON script to load.
// This method is for adding static shapes
// This works perfectly fine ..
parent.add_shape = function(frame)
{
var material = new THREE.MeshLambertMaterial({
color: frame.shape.color,
wireframe: true,
wireframeLinewidth: 0.1,
opacity: 0.5
})
var geometry = new THREE.CylinderGeometry(frame.shape.radius,frame.shape.radius,frame.shape.height,50,50);
// mesh_dict dictionary maps a mesh(shape) to its frame
parent.mesh_dict[frame] = new THREE.Mesh(geometry,material);
var init_orientation = frame.simulation_matrix[0];
var orienter = new THREE.Matrix4();
orienter.elements = [];
//Since simulation_matrix is generated from python, it is a
// list of lists, We need to push it to the elemens of Matrix4 manually ..
for(var i in init_orientation)
{
for(var j in init_orientation[i])
{
orienter.elements.push(init_orientation[i][j]) ;
}
}
parent.mesh_dict[frame].applyMatrix(new THREE.Matrix4());
parent.mesh_dict[frame].applyMatrix(orienter);
parent.scene.add(parent.mesh_dict[frame]);
parent.renderer.render(parent.scene,parent.camera);
}
// This method basically takes the meshes defined in add_shape, and
// applies simulation matrix to it, and requests animation frame for
// animation.
parent.animate = function()
{
for(var frame in JSONObj.frames)
{
// defining simulation_matrix in a var.
var matrix = JSONObj.frames[frame].simulation_matrix[parent.animation_counter];
var animation_matrix = new THREE.Matrix4();
animation_matrix.elements = [];
// pushing it to a Matrix4
for(var i in matrix)
{
for(var j in matrix[i])
{
animation_matrix.elements.push(matrix[i][j]) ;
}
}
console.log(animation_matrix);
console.log(animation_matrix.elements);
// Making sure we are not applying matrix to the earlier transform
//mesh_dict is a dictionary of meshes, used in creating shapes,mapped to the
//frame which contains them
parent.mesh_dict[JSONObj.frames[frame]].applyMatrix(new THREE.Matrix4());
// now applying transform, after setting to identity matrix ...
parent.mesh_dict[JSONObj.frames[frame]].applyMatrix(animation_matrix);
}
console.log(parent.animation_counter);
//update timestep ...
parent.animation_counter++;
// This is to loop over again and again ...
// assuming 10 animations frames
if(parent.animation_counter == 10){ parent.animation_counter = 0; }
requestAnimationFrame(parent.animate);
}
The problem is that I am able to create the multiple shapes, but when I apply simulation matrix to them in the loop, only one of them is animating, that too in very unexpected manner.
Well I have figured out what was wrong. Somehow, all the dictionary parent.mesh_dict[] keys were mapped to a same single object, instead of all objects as required. Now I debugged it, and it is working like a charm. Also your point is valid #WestLangley, as I now use mesh.matrix.identity() to get things done. Thanks, I will close this question now.
I'm using Flash CS6 and I need to save a 32 bit PNG from a vector graphic in 9 different sizes.
16
32
36
48
72
114
128
150
480
How to write a batch export script for that in JSFL?
JSFL Docs (PDF)
I have a script that does what you are looking to do. It doesn't appear that you have really attempted to write any code or show any research attempt so if you do end up using this script I would appreciate the credit.
Select a movie clip on the stage then run this command.
Andrew Doll - Multiple Size PNG Exporter
Note: Before running this script export one PNG image using the desired PNG export settings.
fl.getDocumentDOM.exportPNG() accepts 3 paramaters. The first is the string for the file name. The second is a Boolean value that specifies whether to use the current PNG publish settings (true) or to display the Export PNG dialog box (false). The third is a Boolean value that specifies whether to export only the current frame (true) or to export all frames, with each frame as a separate PNG file (false).
Since this script sets the second paramater to true just be sure that the PNG export settings are already set to 32 bit PNG.
// Multiple Size PNG Exporter
// Copyright © 2014 Andrew Doll
// http://www.andrewdollanimation.com/
/* NOTE:Before running this script export one PNG image using the desired PNG export settings. fl.getDocumentDOM.exportPNG() accepts 3
** paramaters. The first is the string for the file name. The second is a Boolean value that specifies whether to use the current PNG
** publish settings (true) or to display the Export PNG dialog box (false). The third is a Boolean value that specifies whether to export
** only the current frame (true) or to export all frames, with each frame as a separate PNG file (false). Since this script sets the
** second paramater to true just be sure that the PNG export settings are already set to 32 bit PNG.
*/
// Check to see if there is a file open first.
var dom = fl.getDocumentDOM();
if (dom == null)
{
alert("Please open a file.");
}
else
{
var sel = [];
var exportSizeArray = [];
var folderURI = "";
var folderLocation = "";
var pngFileName = "";
var URI = "";
var selWidth;
var selHeight;
var sideToUse;
var scaleAmount;
function setupExportFolder()
{
// Create a folder and file name for the PNG files.
folderLocation = fl.browseForFolderURL("Select a folder.");
if(folderLocation != null)
{
folderURI = folderLocation + "/PNG Exports";
FLfile.createFolder(folderURI);
pngFileName = prompt("What would you like to name the png files?");
}
}
// Check if a movie clip on the stage is selected to export PNG images.
var selectionCheck = dom.selection;
if(!selectionCheck || !selectionCheck.length)
{
alert("Please select a movie clip on the stage.");
}
else
{
// Set up export sizes in this array.
exportSizeArray = [16, 32, 64, 128, 256, 512, 1024];
// Setup export folder
setupExportFolder();
if(folderLocation != null && pngFileName != null)
{
// Copy the selected artwork from the stage.
sel = dom.selection[0];
dom.clipCopy();
// Calculate the amount to scale the symbol by based on the longest side.
function calculateScaleAmount(selWidth, selHeight)
{
if(selWidth >= selHeight)
{
sideToUse = selWidth;
}
else
{
sideToUse = selHeight;
}
scaleAmount = exportSizeArray[i]/sideToUse;
return scaleAmount;
}
// Set the width and height of the symbol. Handle this with the size array.
for (var i = 0; i < exportSizeArray.length; i++)
{
// Create a new FLA document.
fl.createDocument();
dom = fl.getDocumentDOM();
// Resize the document to the current export size.
dom.width = exportSizeArray[i];
dom.height = exportSizeArray[i];
// Paste the artwork to the stage.
dom.clipPaste(true);
sel = dom.selection[0];
dom.setAlignToDocument(true);
selWidth = sel.width;
selHeight = sel.height;
calculateScaleAmount(selWidth, selHeight);
// Scale the artwork to the size of the stage based on the largest side.
dom.scaleSelection(scaleAmount, scaleAmount, "center");
// Align to the center of the stage.
dom.align("vertical center", true);
dom.align("horizontal center", true);
// Output the image.
URI = folderURI + "/" + pngFileName + "_" + exportSizeArray[i] + " x " + exportSizeArray[i] + "_";
dom.exportPNG(URI, true, true);
// Close the temporary FLA without saving.
dom.close(false);
}
}
}
}