Copy link to clipboard
Copied
How would i go about sorting images based on their position on the page? I've read a few articles that mention using array sort method and then checking their x and y values to sort the array. While that works on page items that are perfectly aligned on their x and y axis, it fails on a layout like this. Does anyone have any ideas on how i could create an individual array for Row1, Row2, Row3... etc based on its Y position and then sort them based on their X positon and then create the master array by combining them in sequence?
I am new to javascript so this is a little out of my league. Any help will be much appreciated.
Hi again,
I found an idea that looks promising to address the issues mentioned above. The main problem, as you've probably noticed, is that we cannot simply rely on (x,y) coordinates 'as they are', even after having rounded the values, to extract the implied rows and columns.
When we study the disposition below:
our eyes instantly detect that there should be 3 columns and 5 rows, but this underlying order isn't instantly reached from just sorting the set of coordinates. We need to improve the algor
...Copy link to clipboard
Copied
At the level of detail you use to describe your script it works on the page you show. I suspect a mistake in the sorting code. Can you share it so we can react to it?
Dave
Copy link to clipboard
Copied
Hi Dave. Thanks for the quick response. The image above is an example of a script i have already written. The script takes all the selected images and creates a tightly fit grid like the image above. That's working perfectly.
Now, i am trying to add in a functionality where in a user can move the position of images just by dragging and dropping them in general area of where the image should be. Then, when you run the script again it sorts the array of the entire selection based on the each items postion on the page and then creates the same tightly fit grid.
I decided to do an individual test to see if the sorting works before i include it in the "image grid" script. Here's where i am with that test.
When i run the follwing script on two layouts (image below) it works perfectly well in "TEST 1" when all the boxes are aligned on the "Y" co-ordinate. But fails on "TEST 2" when they are slightly offset. This is critical because i would like to give the user the ease of use of just roughly placing the images, rather than perfectly aligning them.
var blocs = app.selection;
var newArr = blocs.sort(byYX);
function byYX(a,b) {
var
aY = a.geometricBounds[0],
bY = b.geometricBounds[0],
aX = a.geometricBounds[1],
bX = b.geometricBounds[1],
dy = aY-bY,
dx = aX-bX;
return dy?dy:dx;
}
for(var i = 0; i < newArr.length; i++) {
$.write(newArr.contents + "\r")
}
// TEST 1 CONSOLE LOG: 123456789
// TEST 2 CONSOLE LOG: 251346789
I wracked my brain a little more and i think I may have some sort of solution. I added some Math.floor to the x/y values to get an average for the array sort. This seems to work in some cases, but the rounding value is very fickle and needs to be adjusted to get the right setting. Too high and it grabs boxes from rows beyond itself. Too low and it misses items in the same row. Any thoughts on this technique?
var value = .5;
var blocs = app.selection;
var newArr = blocs.sort(byYX);
function byYX(a,b) {
var
aY = Math.floor(a.geometricBounds[0] * value) / value,
bY = Math.floor(b.geometricBounds[0] * value) / value,
aX = Math.floor(a.geometricBounds[1] * value) / value,
bX = Math.floor(b.geometricBounds[1] * value) / value,
dy = aY-bY,
dx = aX-bX;
return dy?dy:dx;
}
for(var i = 0; i < newArr.length; i++) {
$.write(newArr.contents + "\r")
}
Copy link to clipboard
Copied
If this doesn't help, I'll look more deeply this evening:
The first part of a a?b:c statement must be logical. But dy is a number.
Dave
Copy link to clipboard
Copied
No time to test, but I think you have to make a important distinction between x-precision and y-precision:
var X_PRECISION = 1,
Y_PRECISION = 50,
mFLOOR = Math.floor;
var byYX = function F(a, b)
{
a = F.data['_'+a.id];
b = F.data['_'+b.id];
return (a[1]-b[1])||(a[0]-b[0]);
};
byYX.data = {};
var value = .5,
blocs = app.properties.selection || null,
newArr,
i, t, k, o;
if( blocs )
{
o = byYX.data;
i = blocs.length;
while(i--)
{
k = '_'+(t=blocs).id;
t = t.geometricBounds;
o
= [X_PRECISION*mFLOOR(t[1]/X_PRECISION), Y_PRECISION*mFLOOR(t[0]/Y_PRECISION)]; }
newArr = blocs.sort(byYX);
for( i=0 ; i < newArr.length ; ++i )
{
app.select(newArr);
$.sleep(1000);
}
}
@+
Marc
EDIT: The code above is just a raw approximation of the algorithm to illustrate my point. With regard to your purpose, comparing x-locations is not a problem as soon as rows (viz. y-locations) are properly computed. Hence, the whole problem is to determine under which condition two items belong to the same row, in terms of y-location constraint. In my routine I use a high Y_PRECISION factor (50) in order to attract elements with a similar y-location to the same row. But this y-similarity is only based on a basic Math.floor, so my algorithm isn't really smart. In fact, your introductory example shows that the property of belonging-to-the-same-row is much more complex and should take into consideration what x-location is already occupied or still empty. Anyway, my code is intended to show you a way to precompute relevant locations before you sort the array. This has two advantages: first, you will significantly speed up the sorting—as the byYX function doesn't need to re-access DOM objects and their geometric bounds. Secund, you can then separate the data to sort and the sort in itself, which allows you to refine the key values—row and col parameters—independently.
Copy link to clipboard
Copied
Hi again,
I found an idea that looks promising to address the issues mentioned above. The main problem, as you've probably noticed, is that we cannot simply rely on (x,y) coordinates 'as they are', even after having rounded the values, to extract the implied rows and columns.
When we study the disposition below:
our eyes instantly detect that there should be 3 columns and 5 rows, but this underlying order isn't instantly reached from just sorting the set of coordinates. We need to improve the algorithm.
What I suggest is to speculate on the gaps that occur on the ordered sequence of x-coordinates and y-coordinates, respectively. To reveal these gaps, let's sort data along the x-axis first:
The figure above only shows the widths of the rectangles. Each arrow represents a rectangle, and I've ordered the items by increasing x-centers. One can estimate that the element labelled #1 belongs to a new group from this simple fact: its left coordinate (red guide) is higher than the right coordinate (blue guide) of the element #0. This observation will give us a strategy to detect column gaps.
Then, the same method is applied to detect rows. (Sorting by y-values, identifying gaps based on min-max progression.)
At the end of this process, every object has a (column, row) coordinate pair instead of sparse (x,y) values. So we can compute the final order, i.e. the weights for the comparison function.
Here is my implementation of this algorithm:
// ========================================================
// Up2Bottom and Left2Right Sorting Algorithm
// addressing (weakly) sparse rectangles
// ---
// Usage: Select the objects, then run the script
// Target: InDesign CS4/CS5/CS6/CC
// ========================================================
const CS = +CoordinateSpaces.SPREAD_COORDINATES,
AP_MIN = +AnchorPoint.TOP_LEFT_ANCHOR,
AP_CENTER = +AnchorPoint.CENTER_ANCHOR,
AP_MAX = +AnchorPoint.BOTTOM_RIGHT_ANCHOR;
var sel = app.properties.selection || null,
data = [],
r, i, j, k, t, n, w, vMax;
if( sel && 1 < (n=sel.length) )
{
// Collect coordinates and IDs
// --> {min:[xLeft, yTop], weight:[x,y], max:[xRight,yBottom], id}[]
for(i=0 ; i < n && (t=sel) ; ++i )
{
data = {
min: t.resolve(AP_MIN,CS)[0],
weight: t.resolve(AP_CENTER,CS)[0],
max: t.resolve(AP_MAX,CS)[0],
id: t.id
};
}
// Find rows and columns [i.e. y-weights and x-weights]
// ---
for( j=0 ; j < 2 ; ++j )
{
// Sort by center coordinate
// ---
data.sort(function(a,b){return a.weight
- b.weight }); // min > max ==> w++
// ---
for(vMax=(t=data[0]).max
, t.weight =(w=0), i=1 ; (i < n)&&(t=data) ; ++i ) {
if( t.min
> vMax ){ ++w; vMax=t.max ; } t.weight
= w; }
}
// Compute final weights, clean up data, create ID-to-weight access
// ---
for( i=0 ; (i < n)&&(t=data) ; ++i )
{
w = n*t.weight[1] + t.weight[0]; // final weight (y first)
k = '_'+t.id; // ID key
(t.min.length=0)||(t.weight.length=0)||(t.max.length=0);
delete t.min; delete t.weight; delete t.max; delete t.id;
delete data;
data
= w; // ID-to-weight }
// Apply sort --> r
// ---
r = sel.sort(function(a,b){return data['_'+a.id]-data['_'+b.id];});
// Show the resulting order
// ---
for( i=0 ; i < n ; ++i )
{
app.select(r);
$.sleep(1000);
}
}
@+
Marc
Copy link to clipboard
Copied
Marc,
Wow!! What is this, some kind of black magic? Kidding, of course. This is waaay out of my league. I'm blown away. Amazing.
Thank you so much. I tested it with a few scenarios and it worked flawlessly.
Your approach to the problem is smart and makes sense. I understood the logic, now i need to learn how you executed it. I'm going to read your code over and over to figure out what you did there.
Thanks again, Marc. Very cool.
-Tushar
Copy link to clipboard
Copied
Hi everyone,
Thank you for this post, it seems to be very helpful for my use case
I'm looking for a way to sort a collection of page item by order of appearance.
Case study
- I just wrote a script to create QR Code in every page items.
- the data come from a text file > the script will add a qr code in each item on a certain layer
Problem
- the script works well. But, if the designer do not create the item in the correct order, the QR Codes will not match.
- I'm looking for a way to sort the collection. The algorytm above is interesting but don't do exactly what I want (sort more than 1 page > all document / sort page by page not a spead page)
Here is the code
that.run = function () {
// get layer QR-FR
var doc = app.activeDocument;var layers = doc.layers;
try {
var items_fr = layers.item('QR-FR').pageItems;
var items_de = layers.item('QR-DE').pageItems;
var items_it = layers.item('QR-IT').pageItems;
var items_en_cn = layers.item('QR-EN-CN').pageItems;
}
catch (ex) {
throw {
name: 'Error',
message: "The file don't have a correct structure (layers: QR-FR, QR-DE, QR-IT, QR-EN-CN",
fileName: $.fileName,
lineNumber: $.line
};}
// here sort collection !!!!!!
// open a text file
var my_file_fr = new File(CONSTANTS.DATA + '/fr.txt');var my_file_de = new File(CONSTANTS.DATA + '/de.txt');
var my_file_it = new File(CONSTANTS.DATA + '/it.txt');
var my_file_en_cn = new File(CONSTANTS.DATA + '/en-cn.txt');
var array_qr_fr = my_file_fr.readUTF8().split('\n');
var array_qr_de = my_file_de.readUTF8().split('\n');
var array_qr_it = my_file_it.readUTF8().split('\n');
var array_qr_en_cn = my_file_en_cn.readUTF8().split('\n');
// select every items (textframes > add a QR in each one)
generate_qr(items_fr, array_qr_fr);generate_qr(items_de, array_qr_de);
generate_qr(items_it, array_qr_it);
generate_qr(items_en_cn, array_qr_en_cn);
alert('le script est terminé');
}
Any help would be appreciated! Thank you very much!
Best,Bastien
Copy link to clipboard
Copied
I had to add this line at the top of the script in order to see the selection change of the last for loop (InDesign CC2019):
app.scriptPreferences.enableRedraw = true;
Copy link to clipboard
Copied
Hi Stefan,
Yep. This is an opportunity to remind all our colleagues that app.scriptPreferences.enableRedraw is application-persistent. All of us should remember that changing script prefs at that level has good chance to impact others' scripts—unless politely backing up and restoring settings.
Best,
Marc
Copy link to clipboard
Copied
Hi Stefan,
I solved the problem of visibility with InDesign CC 2019 a bit differently for testing purposes and added a text path to the sorted selection where the value of variable i is written as contents to the story of the path.
On with a note:
Marc wrote in reply #5:
What I suggest is to speculate on the gaps that occur on the ordered sequence of x-coordinates and y-coordinates, respectively.
Since Marc's algorithm is based on gaps I suppose it will fail if elements overlap like in my second set of selected rectangles as seen in the screenshot below.
Another sample with a selection of polygons.
Some overlap ( the geometric bounds overlap ), some do not:
In this very special case Tushar's second script from reply #2 where the rounding of geometric bounds is done worked very well:
Regards,
Uwe