Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to override font family for all the text in html in PdfRendererBuilder? #641

Closed
asu2 opened this issue Jan 27, 2021 · 9 comments · Fixed by #669
Closed

How to override font family for all the text in html in PdfRendererBuilder? #641

asu2 opened this issue Jan 27, 2021 · 9 comments · Fixed by #669
Assignees

Comments

@asu2
Copy link

asu2 commented Jan 27, 2021

Hi,

The problem we have is that the html source files are from different vendors/countries so it seems impossible to load all fonts they are using or will use. I'd like to know if there's a way to override font for all the text, regardless which font family is declared for them in CSS (but still keep font style and size if possible), so I can simply specify an unicode-friendly font for all html sources.

Thanks!

@draco1023
Copy link

  1. Register FONT_NAME to PdfRendererBuilder.
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFont(YOUR_FONT_SUPPILER, FONT_NAME);
builder.useCacheStore(PdfRendererBuilder.CacheStore.PDF_FONT_METRICS, new FSDefaultCacheStore());
  1. Parse html content with Jsoup.
  2. Append a style tag to the document head with content * { font-family: FONT_NAME; }.
  3. Find all elements with inline style and append FONT_NAME at the end of font-family declaration if it exists.

@asu2
Copy link
Author

asu2 commented Jan 27, 2021

Thanks for the reply.

I found another way to do this:

  1. Create a subclass from PdfBoxFontResolver then override resolveFont() method to return a 'default' font;
  2. Now comes the ugly: I have to use a lot of reflections to private fields to make my subclass work in PdfBoxRenderer.

I will definitely try your suggestion as I don't like reflection-to-private at all.

@syjer
Copy link
Contributor

syjer commented Jan 27, 2021

hi @asu2 , I think it would be a nice feature to have a customizable Font resolver (edit: for fallback). I'll have a look at what it would mean during the week end.

If you can post your subclass here, it would be a nice start to see what we would need to change/modify.

@asu2
Copy link
Author

asu2 commented Jan 27, 2021

@syjer here you go ;-)

import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.value.FontSpecification;
import com.openhtmltopdf.extend.FSCacheEx;
import com.openhtmltopdf.extend.FSCacheValue;
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.pdfboxout.PdfBoxFSFont;
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver;
import com.openhtmltopdf.pdfboxout.PdfBoxRenderer;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.openhtmltopdf.render.FSFont;
import org.apache.fontbox.ttf.OTFParser;
import org.apache.fontbox.ttf.OpenTypeFont;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class OverriddenFontResolver extends PdfBoxFontResolver {

	private static String[] FONT_FILES = {
		"fonts/NotoSansNaskhArabic-Regular.ttf",
		"fonts/NotoSansHebrew-Regular.ttf",
		"fonts/NotoSansCJKjp-Regular.otf",
		"fonts/NotoSansCJKkr-Regular.otf",
		"fonts/NotoSansCJKsc-Regular.otf",
		"fonts/NotoSans-Regular.ttf",
	};
	private List<FontDescription> fonts;

	// The way I did this is because I have no idea where I can get these parameters correctly!
	public OverriddenFontResolver(PdfBoxFontResolver resolver) {
		super((SharedContext) readField(resolver, "_sharedContext"), (PDDocument) readField(resolver, "_doc"),
			(FSCacheEx<String, FSCacheValue>) readField(resolver, "_fontMetricsCache"), (PdfRendererBuilder.PdfAConformance) readField(resolver, "_pdfAConformance"),
			(Boolean) readField(resolver, "_pdfUaConform"));
	}

	@Override
	public FSFont resolveFont(SharedContext renderingContext, FontSpecification spec) {
		return new PdfBoxFSFont(getFonts(), spec.size);
	}

	private static Object readField(PdfBoxFontResolver resolver, String name) {
		try {
			Field f = PdfBoxFontResolver.class.getDeclaredField(name);
			f.setAccessible(true);
			return f.get(resolver);
		}
		catch (Exception ex) {
			throw new RuntimeException("Failed to read field: " + name, ex);
		}
	}

	private static FontDescription newFontDescription(PDFont font) {
		try {
			Constructor<FontDescription> c = FontDescription.class.getDeclaredConstructor(PDFont.class, IdentValue.class, Integer.TYPE);
			c.setAccessible(true);
			return c.newInstance(font, IdentValue.NORMAL, 400);
		}
		catch (Exception ex) {
			throw new RuntimeException("Failed to create new instance: FontDescription", ex);
		}
	}

	private List<FontDescription> getFonts() {
		if (fonts == null) {
			List<FontDescription> temp = new ArrayList<>();
			for (String f : FONT_FILES) {
				try (InputStream in = new FileInputStream(new File(f))) {
					OTFParser parser = new OTFParser();
					OpenTypeFont otf = parser.parse(in);
					temp.add(newFontDescription(PDType0Font.load((PDDocument) readField(this, "_doc"), otf, false)));
				}
				catch (IOException ex) {
					//todo
					System.err.println("Error in loading font file: " + f + ", " + ex);
				}
			}
			temp.add(newFontDescription(PDType1Font.HELVETICA));
			fonts = temp;
		}
		return fonts;
	}

	public static void main(String[] args) {
		// Replace default resolver.
		PdfRendererBuilder builder = new PdfRendererBuilder() {
			@Override
			public PdfBoxRenderer buildPdfRenderer(Closeable diagnosticConsumer) {
				PdfBoxRenderer renderer = super.buildPdfRenderer(diagnosticConsumer);
				PdfBoxFontResolver resolver = renderer.getFontResolver();
				renderer.getSharedContext().setFontResolver(new OverriddenFontResolver(resolver));
				return renderer;
			}
		};
	}
}

@draco1023
Copy link

if (_pdfAConformance == PdfAConformance.NONE &&
!_pdfUaConform) {
// We don't have a final fallback font for PDF/A documents as serif may not be available
// unless the user has explicitly embedded it.
// For now, we end up with "Serif" built-in font.
// Q: Should this change?
// Q: Should we have a final automatically added font?
fonts.add(resolveFont(ctx, "Serif", size, weight, style, variant));
}

Serif is not working for unicode characters. Maybe we should expose a fallback font property in PdfRendererBuilder and SharedContext, then we can use the fallback font in PdfBoxFontResolver.

@asu2
Copy link
Author

asu2 commented Jan 28, 2021

yeah, an easily-accessible fallback font is exactly what I'm looking for. Currently I'm using your 'css-injection' solution which works very well. Thanks!

@syjer
Copy link
Contributor

syjer commented Jan 28, 2021

looks like exposing the possibility to provide a programmatic way to let the user select the fallback font is the most elegant way.

You register all the fonts that you need as normal and in the builder you pass something like:

builder.defineFallbackFontResolver( (x,y,z,??) -> {your code here; return font name;});

@danfickle danfickle self-assigned this Feb 6, 2021
danfickle added a commit that referenced this issue Mar 15, 2021
Including major refactor of PDF font resolver and working test.
@danfickle
Copy link
Owner

I've opened PR #669. Feel free to have a look and comment. Trying to do a long overdue release tomorrow though. Will merge first.

danfickle added a commit that referenced this issue Mar 17, 2021
danfickle added a commit that referenced this issue Mar 17, 2021
to avoid mega class in PdfBoxFontResolver.
danfickle added a commit that referenced this issue Mar 17, 2021
This optimization was assumable designed to avoid the O(n) lookup of a font family variants. However when n is mostly 4 or under it is unneeded and complicates the code without good reason.

Speaking of premature opts, also added a few performance tweaks to the matching process.
danfickle added a commit that referenced this issue Mar 19, 2021
@IStrangers
Copy link

Add !important after CSS font

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants