I'm reading existing text files containing new-page characters into PROC DOCUMENT, but to paginate them correctly in ODS PDF I have to replay the individual pages separately.
In the file processing I've extracted the 2 title lines of each file into &line3 and &line4, and I'd really like those macro variables to be used in a bookmark for only the 1st page of each file:
%macro import_text(file=, orient=landscape);
filename source catalog 'work.import';
data _null_;
length line $120;
retain i prev_n 0;
infile &file. truncover end = eof;
input line $char120.;
file source(import.source);
if _N_ = 3 then call symput('line3', strip(line));
else if _N_ = 4 then call symput('line4', strip(line));
else if substr(line, 1, 1) = '0C'x then do;
** If first character is page break, then generate replay **;
i + 1;
if i = 1 then do;
put 'replay textfile (where = (_obs_ lt ' _N_ '));';
end;
else do;
put 'replay textfile (where = (' prev_n ' le _obs_ lt ' _N_ '));';
end;
prev_n = _N_;
end;
if eof then do;
put 'replay textfile (where = (' prev_n ' le _obs_));';
end;
run;
options orientation = &orient. nodate;
title " ";
proc document name = import(write);
import textfile = &file. to ^;
obpage \textfile / after;
setlabel ^ "&line3.: &line4.";
%include source(import.source) / source2;
run;
quit;
%mend import_text;
ods listing close;
ods pdf file = "~/osi_listings/output/osi_listings.pdf"
pdftoc = 1 contents = yes;
%import_text(file="~/osi_listings/output/lo_hru.L10")
%import_text(file="~/osi_listings/output/lo_s_dth.L10")
%import_text(file="~/osi_listings/output/lo_s_ae_irr.L10")
ods pdf close;
However, when I generate the PDF files I'm getting bookmarks for every page with the full path name of that file:
How can I update my program to use &line3 and &line4 in the bookmarks instead?
Finally I have resolved the problem using the techniques suggested in a 2010 PhUSE paper by Stephen Griffiths in http://www.lexjansen.com/phuse/2010/ts/TS04.pdf. This method creates bookmarked PDF files accepted by the FDA!
The final program is given here, but the full explanation of the code can be found in VIEWS News 65 (see https://hollandnumerics.org.uk/wordpress/2021/09/views-news-65-2021q3-has-been-published/).
* Final Location for Listings and PDF file *;
%LET filelist = ./output;
%LET filepdf = ./output;
%LET offset = 1;
LIBNAME listings "&filepdf.";
* Name of SAS dataset with Bookmarks *;
%LET bookmark_ds = bookmark_listings;
* Name of PS file *;
%LET bm_ds = osi_bookmarks;
DATA _bookmarks_;
LENGTH filen $30;
group = 'temp ';
lpage = 0;
ltotpage = 0;
opage = 0;
ototpage = 0;
filen = '__temp.xxx';
OUTPUT;
RUN;
%MACRO outpage(
dsout = /* bookmark list */
,fname = /* name of input text file */
,ftype = /* file type */
,title = /* file title */
,orient = landscape /* Orientation */
,prefix = Sex /* Group text */
);
OPTIONS ORIENTATION = &orient.
NODATE NONUMBER;
TITLE " ";
DATA &fname. (KEEP = order group lpage
ltotpage);
LENGTH line2 $150
group $6
;
* Page number *;
RETAIN lpage 1 ltotpage 1;
INFILE "&filelist./&fname..&ftype."
TRUNCOVER END = eof;
INPUT;
* variable to retain the ordering *;
order = _N_;
FILE PRINT;
* at each page break insert a new page *;
SELECT;
WHEN (SUBSTR(_INFILE_, 1, 1) EQ '0C'X) DO;
line2 = SUBSTR(_INFILE_, 2);
PUT _PAGE_ @&offset. line2;
lpage + 1;
END;
WHEN (_INFILE_ EQ '')
PUT @&offset. _INFILE_;
OTHERWISE PUT @&offset. _INFILE_;
END;
* Total pages *;
IF _N_ = 1 THEN DO;
ltotpage =
INPUT(SCAN(_INFILE_, -1), BEST8.);
CALL SYMPUT('lpages',
PUT(ltotpage, ??BEST.));
END;
IF INDEX(UPCASE(STRIP(_INFILE_)),
UPCASE("&prefix.")) EQ 1 THEN DO;
group = STRIP(SCAN(STRIP(_INFILE_),
2, ':'));
OUTPUT;
END;
RUN;
PROC SORT DATA = &fname. OUT = &fname.;
BY group order;
RUN;
DATA &fname. (DROP = order);
SET &fname.;
BY group order;
IF FIRST.group;
LENGTH filen $30 ltitle $200;
filen = "&fname.";
ltitle = "&title.";
RUN;
PROC SORT DATA = &dsout. OUT = _maxp;
BY DESCENDING ototpage;
RUN;
%LET totp=;
DATA _NULL_;
SET _maxp;
IF _N_ = 1
THEN CALL SYMPUT('totp', ototpage);
RUN;
DATA &fname.;
SET &fname.;
opage = lpage + &totp.;
ototpage = ltotpage + &totp.;
RUN;
DATA &dsout.
(WHERE = (group NOT IN ('temp' ' ')));
SET &dsout. &fname.;
* Update total overall pages *;
ototpage = &lpages. + &totp.;
RUN;
%MEND outpage;
%MACRO create_bmfile(
dsin = /* bookmarks data set */
,psout = /* PS file to hold bookmarks */
);
PROC SORT DATA = &dsin.
OUT = &dsin._listings
(KEEP = ltitle opage ototpage filen)
NODUPKEY;
BY ltitle;
RUN;
PROC SORT DATA = &dsin.
OUT = &dsin._groups (KEEP = group)
NODUPKEY;
BY group;
RUN;
PROC SQL;
CREATE TABLE &dsin._template AS
SELECT *
FROM &dsin._groups
,&dsin._listings
ORDER BY
ltitle
,group
;
QUIT;
DATA &dsin._template (DROP = opage ototpage);
SET &dsin._template;
order = opage;
nodatapage = ototpage + 1;
RUN;
PROC SORT DATA = &dsin. out=&dsin.1;
BY ltitle group;
RUN;
DATA &dsin.2;
MERGE &dsin._template (IN = a)
&dsin.1 (IN = b)
;
BY ltitle group;
* groups not in files *;
IF a AND NOT b THEN DO;
opage = nodatapage;
END;
RUN;
PROC SORT DATA = &dsin.2 OUT = &dsin.3;
BY group order opage;
RUN;
PROC FREQ DATA = &dsin.3 NOPRINT;
TABLE group / OUT = &dsin._group_sub
(DROP = percent
RENAME = (count = sub_bm));
RUN;
DATA &dsin.4;
MERGE &dsin.3
&dsin._group_sub
;
BY group;
RUN;
DATA &dsin.4 (KEEP = group ltitle order opage
sub_bm filen)
listings.&bookmark_ds
(KEEP = group ltitle filen opage
sub_bm filen)
;
SET &dsin.4;
* Make the number negative so that the
bookmark is closed *;
sub_bm = sub_bm * -1;
RUN;
DATA _NULL_;
FILE "&psout." LS = 1000;
SET &dsin.4;
BY group order;
IF FIRST.group THEN DO;
PUT "[/Count " sub_bm "/Title (" group
" ) /Page " opage " /OUT pdfmark";
END;
IF FIRST.order THEN DO;
PUT "[/Title (" ltitle " ) /Page "
opage " /OUT pdfmark";
END;
RUN;
%MEND create_bmfile;
PROC TEMPLATE;
DEFINE STYLE groupfile;
parent = styles.Printer;
STYLE fonts FROM fonts /
"BatchFixedFont" =
("Courier New, Courier", 9.5PT)
"docFont" =
("Courier New, Courier", 9.5PT)
"FixedFont" =
("Courier New, Courier", 9.5PT)
;
END;
RUN;
ODS PDF FILE = "&filepdf./osi_listings.pdf"
STYLE = groupfile;
ODS PDF NOBOOKMARKGEN;
%outpage(
dset = &bm_ds.
,fname = test_listings1
,ftype = txt
,title = %nrstr(Listing 99.1)
,prefix = Sex
);
%outpage(
dset = &bm_ds.
,fname = test_listings2
,ftype = txt
,title = %nrstr(Listing 99.2)
,prefix = Sex
);
%outpage(
dset = &bm_ds.
,fname = test_listings3
,ftype = txt
,title = %nrstr(Listing 99.3)
,prefix = Sex
);
DATA _NULL_;
FILE PRINT;
PUT _PAGE_ @&offset.
'There is no data available.';
RUN;
ODS PDF CLOSE;
DATA &bm_ds.;
SET _bookmarks_;
RUN;
ODS _ALL_ CLOSE;
%create_bmfile(
dsin = &bm_ds.
,psout = &filepdf./&bm_ds..ps);
FILENAME cmd PIPE 'gswin64c -dBATCH -dNOPAUSE
-sDEVICE=pdfwrite
-sOutputFile=osi_listings_bm.pdf
osi_listings.pdf osi_bookmarks.ps';
DATA _NULL_;
INFILE cmd;
INPUT;
PUT _INFILE_;
RUN;
Note that some of the filenames may require folder locations to be added when you run this code on your own SAS platform.
Well, if someone like you doesn't know how to do it...
Well, let me think about it and see if anything bubbles up to the surface if I examine the problem but deliberately don't think about it for a while.
Jim
Hi Jim,
I think the issue is centred around IMPORT TO, because ODS PROCLABEL and SETLABEL both fail to update the bookmark text.
What would be really nice would be being able to update/remove the bookmarks individually for each page, but I think that may not be possible in this release! 🙄
I'll look forward to your thoughts............Phil
Version 2 of my macro copies the textfile into separate document folders and then uses REPLAY with WHERE to select each page. This gives me 2-level bookmarks, as I can now show the BY value from each page too, but I'm still seeing duplicate bookmarks with 1 bookmark for every page.
In version 3 I'm going to try splitting up the input file into pages before using IMPORT in PROC DOCUMENT, and then I'm hoping to be able to switch BOOKMARKGEN on or off for each page.
Can you post the steps you are actually running without the macro to generate them? For example for two files where one of them has at least two pages.
Once you figure out how to change the code the generate the PDF file you can then modify the macro to generate that code.
Hi Tom,
The following steps are being done in version 3 of my program and macro for each input text file:
Repeat of these steps for each input text file.
The resulting PDF file looks like this, where text_listings1.txt has 2 pages of "sex: M", but only 1 page is seen in the bookmarks. This means that I now have the correct number of bookmarks, but the top level containing the listing number and title are duplicated. The 3 listing text files shown below are attached to this post.
Using REPLAY to display the whole folder at the same time arranges the bookmarks as pages under the top level of the listing, but does not omit the duplicates. This is because the underlying data in the document has no information about whether a bookmark should be created or not, so a bookmark is created anyway.
I suspect that the solution is probably post-processing of the full bookmark list to remove the duplicates using software other than SAS!
Does that make any sense at all?..................Phil
Finally I have resolved the problem using the techniques suggested in a 2010 PhUSE paper by Stephen Griffiths in http://www.lexjansen.com/phuse/2010/ts/TS04.pdf. This method creates bookmarked PDF files accepted by the FDA!
The final program is given here, but the full explanation of the code can be found in VIEWS News 65 (see https://hollandnumerics.org.uk/wordpress/2021/09/views-news-65-2021q3-has-been-published/).
* Final Location for Listings and PDF file *;
%LET filelist = ./output;
%LET filepdf = ./output;
%LET offset = 1;
LIBNAME listings "&filepdf.";
* Name of SAS dataset with Bookmarks *;
%LET bookmark_ds = bookmark_listings;
* Name of PS file *;
%LET bm_ds = osi_bookmarks;
DATA _bookmarks_;
LENGTH filen $30;
group = 'temp ';
lpage = 0;
ltotpage = 0;
opage = 0;
ototpage = 0;
filen = '__temp.xxx';
OUTPUT;
RUN;
%MACRO outpage(
dsout = /* bookmark list */
,fname = /* name of input text file */
,ftype = /* file type */
,title = /* file title */
,orient = landscape /* Orientation */
,prefix = Sex /* Group text */
);
OPTIONS ORIENTATION = &orient.
NODATE NONUMBER;
TITLE " ";
DATA &fname. (KEEP = order group lpage
ltotpage);
LENGTH line2 $150
group $6
;
* Page number *;
RETAIN lpage 1 ltotpage 1;
INFILE "&filelist./&fname..&ftype."
TRUNCOVER END = eof;
INPUT;
* variable to retain the ordering *;
order = _N_;
FILE PRINT;
* at each page break insert a new page *;
SELECT;
WHEN (SUBSTR(_INFILE_, 1, 1) EQ '0C'X) DO;
line2 = SUBSTR(_INFILE_, 2);
PUT _PAGE_ @&offset. line2;
lpage + 1;
END;
WHEN (_INFILE_ EQ '')
PUT @&offset. _INFILE_;
OTHERWISE PUT @&offset. _INFILE_;
END;
* Total pages *;
IF _N_ = 1 THEN DO;
ltotpage =
INPUT(SCAN(_INFILE_, -1), BEST8.);
CALL SYMPUT('lpages',
PUT(ltotpage, ??BEST.));
END;
IF INDEX(UPCASE(STRIP(_INFILE_)),
UPCASE("&prefix.")) EQ 1 THEN DO;
group = STRIP(SCAN(STRIP(_INFILE_),
2, ':'));
OUTPUT;
END;
RUN;
PROC SORT DATA = &fname. OUT = &fname.;
BY group order;
RUN;
DATA &fname. (DROP = order);
SET &fname.;
BY group order;
IF FIRST.group;
LENGTH filen $30 ltitle $200;
filen = "&fname.";
ltitle = "&title.";
RUN;
PROC SORT DATA = &dsout. OUT = _maxp;
BY DESCENDING ototpage;
RUN;
%LET totp=;
DATA _NULL_;
SET _maxp;
IF _N_ = 1
THEN CALL SYMPUT('totp', ototpage);
RUN;
DATA &fname.;
SET &fname.;
opage = lpage + &totp.;
ototpage = ltotpage + &totp.;
RUN;
DATA &dsout.
(WHERE = (group NOT IN ('temp' ' ')));
SET &dsout. &fname.;
* Update total overall pages *;
ototpage = &lpages. + &totp.;
RUN;
%MEND outpage;
%MACRO create_bmfile(
dsin = /* bookmarks data set */
,psout = /* PS file to hold bookmarks */
);
PROC SORT DATA = &dsin.
OUT = &dsin._listings
(KEEP = ltitle opage ototpage filen)
NODUPKEY;
BY ltitle;
RUN;
PROC SORT DATA = &dsin.
OUT = &dsin._groups (KEEP = group)
NODUPKEY;
BY group;
RUN;
PROC SQL;
CREATE TABLE &dsin._template AS
SELECT *
FROM &dsin._groups
,&dsin._listings
ORDER BY
ltitle
,group
;
QUIT;
DATA &dsin._template (DROP = opage ototpage);
SET &dsin._template;
order = opage;
nodatapage = ototpage + 1;
RUN;
PROC SORT DATA = &dsin. out=&dsin.1;
BY ltitle group;
RUN;
DATA &dsin.2;
MERGE &dsin._template (IN = a)
&dsin.1 (IN = b)
;
BY ltitle group;
* groups not in files *;
IF a AND NOT b THEN DO;
opage = nodatapage;
END;
RUN;
PROC SORT DATA = &dsin.2 OUT = &dsin.3;
BY group order opage;
RUN;
PROC FREQ DATA = &dsin.3 NOPRINT;
TABLE group / OUT = &dsin._group_sub
(DROP = percent
RENAME = (count = sub_bm));
RUN;
DATA &dsin.4;
MERGE &dsin.3
&dsin._group_sub
;
BY group;
RUN;
DATA &dsin.4 (KEEP = group ltitle order opage
sub_bm filen)
listings.&bookmark_ds
(KEEP = group ltitle filen opage
sub_bm filen)
;
SET &dsin.4;
* Make the number negative so that the
bookmark is closed *;
sub_bm = sub_bm * -1;
RUN;
DATA _NULL_;
FILE "&psout." LS = 1000;
SET &dsin.4;
BY group order;
IF FIRST.group THEN DO;
PUT "[/Count " sub_bm "/Title (" group
" ) /Page " opage " /OUT pdfmark";
END;
IF FIRST.order THEN DO;
PUT "[/Title (" ltitle " ) /Page "
opage " /OUT pdfmark";
END;
RUN;
%MEND create_bmfile;
PROC TEMPLATE;
DEFINE STYLE groupfile;
parent = styles.Printer;
STYLE fonts FROM fonts /
"BatchFixedFont" =
("Courier New, Courier", 9.5PT)
"docFont" =
("Courier New, Courier", 9.5PT)
"FixedFont" =
("Courier New, Courier", 9.5PT)
;
END;
RUN;
ODS PDF FILE = "&filepdf./osi_listings.pdf"
STYLE = groupfile;
ODS PDF NOBOOKMARKGEN;
%outpage(
dset = &bm_ds.
,fname = test_listings1
,ftype = txt
,title = %nrstr(Listing 99.1)
,prefix = Sex
);
%outpage(
dset = &bm_ds.
,fname = test_listings2
,ftype = txt
,title = %nrstr(Listing 99.2)
,prefix = Sex
);
%outpage(
dset = &bm_ds.
,fname = test_listings3
,ftype = txt
,title = %nrstr(Listing 99.3)
,prefix = Sex
);
DATA _NULL_;
FILE PRINT;
PUT _PAGE_ @&offset.
'There is no data available.';
RUN;
ODS PDF CLOSE;
DATA &bm_ds.;
SET _bookmarks_;
RUN;
ODS _ALL_ CLOSE;
%create_bmfile(
dsin = &bm_ds.
,psout = &filepdf./&bm_ds..ps);
FILENAME cmd PIPE 'gswin64c -dBATCH -dNOPAUSE
-sDEVICE=pdfwrite
-sOutputFile=osi_listings_bm.pdf
osi_listings.pdf osi_bookmarks.ps';
DATA _NULL_;
INFILE cmd;
INPUT;
PUT _INFILE_;
RUN;
Note that some of the filenames may require folder locations to be added when you run this code on your own SAS platform.
The final PDF and bookmarks look like this:
Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!
Learn how use the CAT functions in SAS to join values from multiple variables into a single value.
Find more tutorials on the SAS Users YouTube channel.