Compare commits

..

21 commits

Author SHA1 Message Date
dependabot[bot]
b2f39378e8
Bump junit-jupiter from 5.7.1 to 5.7.2 (#23)
Bumps [junit-jupiter](https://github.com/junit-team/junit5) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.7.1...r5.7.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 08:50:48 +01:00
dependabot[bot]
4610003595
Bump tiles-maven-plugin from 2.20 to 2.21 (#22)
Bumps [tiles-maven-plugin](https://github.com/repaint-io/maven-tiles) from 2.20 to 2.21.
- [Release notes](https://github.com/repaint-io/maven-tiles/releases)
- [Changelog](https://github.com/repaint-io/maven-tiles/blob/master/CHANGELOG.adoc)
- [Commits](https://github.com/repaint-io/maven-tiles/compare/tiles-maven-plugin-2.20...tiles-maven-plugin-2.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:31:50 +01:00
dependabot-preview[bot]
86c3cdac75
Upgrade to GitHub-native Dependabot (#21)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-04-30 10:27:38 +01:00
dependabot-preview[bot]
d2df552fa2
Bump lombok from 1.18.18 to 1.18.20 (#20) 2021-04-02 06:20:47 +00:00
dependabot-preview[bot]
842296af3e
Bump tiles-maven-plugin from 2.19 to 2.20 (#19) 2021-03-29 06:49:55 +00:00
dependabot-preview[bot]
40c2d99c47
Bump junit-jupiter from 5.7.0 to 5.7.1 (#18) 2021-02-05 06:23:39 +00:00
dependabot-preview[bot]
724b724272
Bump lombok from 1.18.16 to 1.18.18 (#17) 2021-01-29 06:24:36 +00:00
dependabot-preview[bot]
e11a6a1416
Bump assertj-core from 3.18.1 to 3.19.0 (#16) 2021-01-25 06:23:19 +00:00
dependabot-preview[bot]
72309329c0
Bump tiles-maven-plugin from 2.18 to 2.19 (#15) 2020-12-10 06:31:08 +00:00
dependabot-preview[bot]
07ef236658
Bump assertj-core from 3.18.0 to 3.18.1 (#14) 2020-11-11 06:45:58 +00:00
dependabot-preview[bot]
a5ce8b546b
Bump assertj-core from 3.17.2 to 3.18.0 (#13) 2020-10-26 06:45:53 +00:00
dependabot-preview[bot]
d5dfccab17
Bump lombok from 1.18.14 to 1.18.16 (#12)
Bumps [lombok](https://github.com/rzwitserloot/lombok) from 1.18.14 to 1.18.16.
- [Release notes](https://github.com/rzwitserloot/lombok/releases)
- [Changelog](https://github.com/rzwitserloot/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/rzwitserloot/lombok/compare/v1.18.14...v1.18.16)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-16 09:25:55 +01:00
dependabot-preview[bot]
e827b42df3
Bump lombok from 1.18.12 to 1.18.14 (#11)
Bumps [lombok](https://github.com/rzwitserloot/lombok) from 1.18.12 to 1.18.14.
- [Release notes](https://github.com/rzwitserloot/lombok/releases)
- [Changelog](https://github.com/rzwitserloot/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/rzwitserloot/lombok/compare/v1.18.12...v1.18.14)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-09 08:20:57 +01:00
9cece23736
Merge pull request #10 from kemitix/dependabot/maven/io.repaint.maven-tiles-maven-plugin-2.18
Bump tiles-maven-plugin from 2.17 to 2.18
2020-10-05 11:32:27 +01:00
dependabot-preview[bot]
8c3c5f22c1
Bump tiles-maven-plugin from 2.17 to 2.18
Bumps [tiles-maven-plugin](https://github.com/repaint-io/maven-tiles) from 2.17 to 2.18.
- [Release notes](https://github.com/repaint-io/maven-tiles/releases)
- [Changelog](https://github.com/repaint-io/maven-tiles/blob/master/CHANGELOG.adoc)
- [Commits](https://github.com/repaint-io/maven-tiles/compare/tiles-maven-plugin-2.17...tiles-maven-plugin-2.18)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-10-05 06:47:02 +00:00
dependabot-preview[bot]
25ea1c6554 Bump junit-jupiter from 5.6.2 to 5.7.0
Bumps [junit-jupiter](https://github.com/junit-team/junit5) from 5.6.2 to 5.7.0.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.6.2...r5.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-14 06:54:29 +00:00
dependabot-preview[bot]
e8317aaa9e Bump assertj-core from 3.17.1 to 3.17.2
Bumps [assertj-core](https://github.com/joel-costigliola/assertj-core) from 3.17.1 to 3.17.2.
- [Release notes](https://github.com/joel-costigliola/assertj-core/releases)
- [Commits](https://github.com/joel-costigliola/assertj-core/compare/assertj-core-3.17.1...assertj-core-3.17.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-07 06:56:00 +00:00
dependabot-preview[bot]
ee5f8ba78c Bump assertj-core from 3.17.0 to 3.17.1
Bumps [assertj-core](https://github.com/joel-costigliola/assertj-core) from 3.17.0 to 3.17.1.
- [Release notes](https://github.com/joel-costigliola/assertj-core/releases)
- [Commits](https://github.com/joel-costigliola/assertj-core/compare/assertj-core-3.17.0...assertj-core-3.17.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-31 06:54:29 +00:00
dependabot-preview[bot]
d3561c9b21 Bump assertj-core from 3.16.1 to 3.17.0
Bumps [assertj-core](https://github.com/joel-costigliola/assertj-core) from 3.16.1 to 3.17.0.
- [Release notes](https://github.com/joel-costigliola/assertj-core/releases)
- [Commits](https://github.com/joel-costigliola/assertj-core/compare/assertj-core-3.16.1...assertj-core-3.17.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-24 06:55:53 +00:00
dependabot-preview[bot]
c3f2b6c619 Bump assertj-core from 3.16.0 to 3.16.1
Bumps [assertj-core](https://github.com/joel-costigliola/assertj-core) from 3.16.0 to 3.16.1.
- [Release notes](https://github.com/joel-costigliola/assertj-core/releases)
- [Commits](https://github.com/joel-costigliola/assertj-core/compare/assertj-core-3.16.0...assertj-core-3.16.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-11 09:15:15 +00:00
272fe20e4c
Update github actions (#3)
* Update github actions

* Bump tiles-maven-plugin to 2.17 for jdk 14 compatibility

* Require JDK 11+
2020-07-11 10:11:52 +01:00
20 changed files with 243 additions and 746 deletions

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: maven
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

34
.github/release-drafter.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name-template: 'v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
labels:
- 'chore'
- 'dependencies'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
exclude-labels:
- 'skip-changelog'
template: |
## Changes
$CHANGES

17
.github/stale.yaml vendored Normal file
View file

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

23
.github/workflows/build-maven.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: maven-build
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 11, 14 ]
steps:
- uses: kamiazya/setup-graphviz@v1
- uses: actions/checkout@v2
- name: setup-jdk-${{ matrix.java }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- name: build-jar
run: mvn -B install

View file

@ -1,19 +1,20 @@
name: Deploy to Sonatype Nexus name: sonatype-deploy
on: on:
release: push:
types: [created] tags:
- "v*"
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# - uses: kamiazya/setup-graphviz@v1 - uses: kamiazya/setup-graphviz@v1
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 8
- name: Build with Maven - name: Build with Maven
run: mvn -B install run: mvn -B install
- name: Nexus Repo Publish - name: Nexus Repo Publish

14
.github/workflows/draft-release.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: draft-release
on:
push:
branches:
- master
jobs:
update_draft_release:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5.11.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,26 +0,0 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: '*'
pull_request:
branches: '*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 11, 13 ]
steps:
# - uses: kamiazya/setup-graphviz@v1
- uses: actions/checkout@v2
- name: setup-java-${{ matrix.java }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- name: install
run: mvn -B install

View file

@ -14,6 +14,10 @@ font size and/or overflow into additional rectangles.
</dependency> </dependency>
``` ```
## Requirements
* JDK 11+
## Usage ## Usage
### Word Wrap ### Word Wrap

12
pom.xml
View file

@ -14,17 +14,13 @@
<version>DEV-SNAPSHOT</version> <version>DEV-SNAPSHOT</version>
<properties> <properties>
<java.version>11</java.version> <tiles-maven-plugin.version>2.21</tiles-maven-plugin.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<tiles-maven-plugin.version>2.16</tiles-maven-plugin.version>
<kemitix-maven-tiles.version>2.6.0</kemitix-maven-tiles.version> <kemitix-maven-tiles.version>2.6.0</kemitix-maven-tiles.version>
<kemitix-checkstyle.version>5.4.0</kemitix-checkstyle.version> <kemitix-checkstyle.version>5.4.0</kemitix-checkstyle.version>
<lombok.version>1.18.12</lombok.version> <lombok.version>1.18.20</lombok.version>
<junit.version>5.6.2</junit.version> <junit.version>5.7.2</junit.version>
<assertj.version>3.16.0</assertj.version> <assertj.version>3.19.0</assertj.version>
<mockito.version>3.3.3</mockito.version> <mockito.version>3.3.3</mockito.version>
<jqwik.version>1.2.7</jqwik.version> <jqwik.version>1.2.7</jqwik.version>
</properties> </properties>

View file

@ -2,7 +2,6 @@ package net.kemitix.text.fit;
import java.awt.*; import java.awt.*;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
public interface BoxFitter { public interface BoxFitter {
@ -12,10 +11,4 @@ public interface BoxFitter {
Graphics2D graphics2D, Graphics2D graphics2D,
Rectangle2D box Rectangle2D box
); );
int fit(
String text,
Function<Integer, Font> fontFactory,
Graphics2D graphics2D,
List<Rectangle2D> box
);
} }

View file

@ -5,12 +5,9 @@ import lombok.RequiredArgsConstructor;
import java.awt.*; import java.awt.*;
import java.awt.font.FontRenderContext; import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Fit the text to a box by finding the best font size and word wrapping to fill * Fit the text to a box by finding the best font size and word wrapping to fill
@ -19,7 +16,6 @@ import java.util.stream.Stream;
@RequiredArgsConstructor @RequiredArgsConstructor
class BoxFitterImpl implements BoxFitter { class BoxFitterImpl implements BoxFitter {
public static final int MAX_FONT_SIZE = 10_000;
private final WordWrapper wordWrapper; private final WordWrapper wordWrapper;
@Override @Override
@ -29,22 +25,10 @@ class BoxFitterImpl implements BoxFitter {
Graphics2D graphics2D, Graphics2D graphics2D,
Rectangle2D box Rectangle2D box
) { ) {
return fit(text, fontFactory, graphics2D, int fit = fitMinMax(0, (int) box.getHeight(),
Collections.singletonList(box)); new FitEnvironment(text, fontFactory, graphics2D, box));
}
@Override
public int fit(
String text,
Function<Integer, Font> fontFactory,
Graphics2D graphics2D,
List<Rectangle2D> boxes
) {
int fit = fitMinMax(0, MAX_FONT_SIZE,
new FitEnvironment(text, fontFactory, graphics2D,
boxes));
if (fit <= 2) { if (fit <= 2) {
throw new NotEnoughSpace(0); throw new IllegalArgumentException("The text is too long to fit");
} }
return fit; return fit;
} }
@ -55,39 +39,25 @@ class BoxFitterImpl implements BoxFitter {
FitEnvironment e FitEnvironment e
) { ) {
int mid = (max + min) / 2; int mid = (max + min) / 2;
if (mid == min && mid + 1 == max) { if (mid == min){
return mid; return mid;
} }
Font font = e.getFont(mid); Font font = e.getFont(mid);
try { List<String> lines = wrapLines(font, e);
List<List<Rectangle2D>> linesPerBox = wrapLines(font, e); List<Rectangle2D> lineSizes =
if (tooManyLines(linesPerBox, e.boxes)) { lineSizes(font, lines, e.fontRenderContext());
return fitMinMax(min, mid, e); if (sumLineHeights(lineSizes) > e.boxHeight() ||
} maxLineWidth(lineSizes) > e.boxWidth()) {
} catch (WordTooLong | NotEnoughSpace err) {
return fitMinMax(min, mid, e); return fitMinMax(min, mid, e);
} }
return fitMinMax(mid, max, e); return fitMinMax(mid, max, e);
} }
private boolean tooManyLines( private List<String> wrapLines(
List<List<Rectangle2D>> linesPerBox,
List<Rectangle2D> boxes
) {
return StreamZipper.zip(linesPerBox, boxes,
(stringBounds, box) ->
sumLineHeights(stringBounds) > box.getHeight()
).anyMatch(b -> b);
}
private List<List<Rectangle2D>> wrapLines(
Font font, Font font,
FitEnvironment e FitEnvironment e
) { ) {
return wordWrapper.wrap(e.text, font, e.graphics2D, e.boxes) return wordWrapper.wrap(e.text, font, e.graphics2D, e.boxWidth());
.stream()
.map(l -> lineSizes(font, l, e.fontRenderContext()))
.collect(Collectors.toList());
} }
private List<Rectangle2D> lineSizes( private List<Rectangle2D> lineSizes(
@ -101,10 +71,13 @@ class BoxFitterImpl implements BoxFitter {
} }
private int sumLineHeights(List<Rectangle2D> lineSizes) { private int sumLineHeights(List<Rectangle2D> lineSizes) {
return lineSizes.stream() return lineSizes.stream().map(Rectangle2D::getHeight)
.map(Rectangle2D::getHeight) .mapToInt(Double::intValue).sum();
.mapToInt(Double::intValue) }
.sum();
private int maxLineWidth(List<Rectangle2D> lineSizes) {
return lineSizes.stream().map(Rectangle2D::getWidth)
.mapToInt(Double::intValue).max().orElse(0);
} }
@RequiredArgsConstructor @RequiredArgsConstructor
@ -112,11 +85,18 @@ class BoxFitterImpl implements BoxFitter {
private final String text; private final String text;
private final Function<Integer, Font> fontFactory; private final Function<Integer, Font> fontFactory;
private final Graphics2D graphics2D; private final Graphics2D graphics2D;
private final List<Rectangle2D> boxes; private final Rectangle2D box;
public Font getFont(int size) { public Font getFont(int size) {
return fontFactory.apply(size); return fontFactory.apply(size);
} }
public int boxWidth() {
return (int) box.getWidth();
}
public int boxHeight() {
return (int) box.getHeight();
}
public FontRenderContext fontRenderContext() { public FontRenderContext fontRenderContext() {
return graphics2D.getFontRenderContext(); return graphics2D.getFontRenderContext();

View file

@ -1,10 +0,0 @@
package net.kemitix.text.fit;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class NotEnoughSpace extends RuntimeException {
private final int excessWordCount;
}

View file

@ -1,60 +0,0 @@
package net.kemitix.text.fit;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.IntFunction;
import java.util.stream.Stream;
import static java.lang.Math.min;
import static java.util.stream.IntStream.range;
/**
* Utility to zip two {@link Stream}s together.
*
* @author Paul Campbell (pcampbell@kemitix.net)
*/
public final class StreamZipper {
private StreamZipper() {
throw new UnsupportedOperationException();
}
/**
* Zip two {@link Stream}s together.
*
* <p>The resulting stream will contain only as many items as the shortest of the two lists.</p>
*
* @param a the first List
* @param b the second List
* @param zipper the function to zip an item from each list
* @param <A> the type of the first list
* @param <B> the type of the second list
* @param <C> the type of the joined items
* @return a Stream of the joined items
*/
public static <A, B, C> Stream<C> zip(
final List<A> a,
final List<B> b,
final BiFunction<A, B, C> zipper
) {
return range(0, limit(a, b))
.mapToObj(tuple(a, b, zipper));
}
private static <A, B> int limit(
final List<A> a,
final List<B> b
) {
return min(a.size(), b.size());
}
private static <A, B, C> IntFunction<C> tuple(
final List<A> a,
final List<B> b,
final BiFunction<A, B, C> zipper
) {
return i -> zipper.apply(a.get(i), b.get(i));
}
}

View file

@ -1,11 +1,10 @@
package net.kemitix.text.fit; package net.kemitix.text.fit;
import lombok.Getter;
import java.awt.*; import java.awt.*;
import java.awt.font.FontRenderContext; import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -17,78 +16,31 @@ class TextLineWrapImpl implements WordWrapper {
Font font, Font font,
Graphics2D graphics2D, Graphics2D graphics2D,
int width int width
) {
return wrap(text, font, graphics2D,
Collections.singletonList(
new Rectangle(width, Integer.MAX_VALUE)))
.get(0);
}
@Override
public List<List<String>> wrap(
String text,
Font font,
Graphics2D graphics2D,
List<Rectangle2D> boxes
) { ) {
String source = String.join(" ", text.split("\n")); String source = String.join(" ", text.split("\n"));
List<Word> words = wordLengths(source.split(" "), font, graphics2D); List<Word> words = wordLengths(source.split(" "), font, graphics2D);
return wrapWords(words, boxes); return wrapWords(words, width);
} }
private List<List<String>> wrapWords( private List<String> wrapWords(List<Word> words, int width) {
List<Word> words,
List<Rectangle2D> boxes
) {
Deque<Word> wordQ = new ArrayDeque<>(words);
List<List<String>> wrappings = boxes.stream()
.map(rectangle2D -> {
double width = rectangle2D.getWidth();
double height = rectangle2D.getHeight();
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
int bottom = 0;
int end = 0; int end = 0;
Deque<Word> lineQ = new ArrayDeque<>(); List<String> line = new ArrayList<>();
while (!wordQ.isEmpty()) { for (Word word : words) {
Word word = wordQ.pop();
if ((bottom + word.height) > height) {
wordQ.add(word);
lineQ.forEach(wordQ::push);
return removeBlankLines(lines);
}
if (end == 0 && word.width > width) {
throw new WordTooLong(word.word);
}
if ((end + word.width) > width) { if ((end + word.width) > width) {
lines.add(wordsAsString((Deque<Word>) lineQ)); lines.add(String.join(" ", line));
lineQ.clear(); line.clear();
end = 0; end = 0;
bottom += word.height;
} }
lineQ.add(word); line.add(word.word);
end += word.width; end += word.width;
} }
lines.add(wordsAsString(lineQ)); lines.add(String.join(" ", line));
return removeBlankLines(lines);
}).collect(Collectors.toList());
if (wordQ.isEmpty()) {
return wrappings;
}
throw new NotEnoughSpace(wordQ.size());
}
private List<String> removeBlankLines(List<String> lines) {
return lines.stream() return lines.stream()
.filter(l -> l.length() > 0) .filter(l -> l.length() > 0)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private String wordsAsString(Deque<Word> lineQ) {
return lineQ.stream()
.map(Word::getWord)
.collect(Collectors.joining(" "));
}
private List<Word> wordLengths(String[] words, Font font, Graphics2D graphics2D) { private List<Word> wordLengths(String[] words, Font font, Graphics2D graphics2D) {
FontRenderContext fontRenderContext = graphics2D.getFontRenderContext(); FontRenderContext fontRenderContext = graphics2D.getFontRenderContext();
return Arrays.stream(words) return Arrays.stream(words)
@ -97,16 +49,13 @@ class TextLineWrapImpl implements WordWrapper {
} }
private static class Word { private static class Word {
@Getter
private final String word; private final String word;
private final int width; private final int width;
private final int height;
public Word(String word, Font font, FontRenderContext fontRenderContext) { public Word(String word, Font font, FontRenderContext fontRenderContext) {
this.word = word; this.word = word;
Rectangle2D stringBounds = font.getStringBounds(word + " ", fontRenderContext); Rectangle2D stringBounds = font.getStringBounds(word + " ", fontRenderContext);
this.width = Double.valueOf(stringBounds.getWidth()).intValue(); this.width = Double.valueOf(stringBounds.getWidth()).intValue();
this.height = Double.valueOf(stringBounds.getHeight()).intValue();
} }
} }

View file

@ -1,24 +0,0 @@
package net.kemitix.text.fit;
public class Tuple<A, B> {
private final A partA;
private final B partB;
private Tuple(final A partA, final B partB) {
this.partA = partA;
this.partB = partB;
}
public static <A, B> Tuple<A, B> of(final A a, final B b) {
return new Tuple<>(a, b);
}
public A get1() {
return partA;
}
public B get2() {
return partB;
}
}

View file

@ -1,10 +0,0 @@
package net.kemitix.text.fit;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class WordTooLong extends RuntimeException {
private final String longWord;
}

View file

@ -1,49 +1,13 @@
package net.kemitix.text.fit; package net.kemitix.text.fit;
import java.awt.*; import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.List; import java.util.List;
public interface WordWrapper { public interface WordWrapper {
/**
* Wraps the text using the font in the graphics context to fit within the
* width.
*
* @param text the text to be line wrapped
* @param font the font to calculate character widths from
* @param graphics2D the context into which the font would be rendered
* @param width the maximum width of each line in pixels
* @return a list of the each line of text
* @throws NotEnoughSpace if there are more than {@link Integer#MAX_VALUE}
* lines - so not likely.
* @throws WordTooLong if there is a word that is too long to fit on a line
* by itself.
*/
List<String> wrap( List<String> wrap(
String text, String text,
Font font, Font font,
Graphics2D graphics2D, Graphics2D graphics2D,
int width int width
); );
/**
* Wraps the text using the font in the graphics context to fit within the
* boxes, filling them in order.
*
* @param text the text to be line wrapped
* @param font the font to calculate character widths from
* @param graphics2D the context into which the font would be rendered
* @param boxes the list of rectangles to fit each line within.
* @return a list of the each line of text
* @throws NotEnoughSpace if there are more lines than can be fitted in the
* boxes provided.
* @throws WordTooLong if there is a word that is too long to fit on a line
* by itself. Not likely as this would simply force smaller fonts.
*/
List<List<String>> wrap(
String text,
Font font,
Graphics2D graphics2D,
List<Rectangle2D> boxes
);
} }

View file

@ -2,7 +2,6 @@ package net.kemitix.text.fit;
import org.assertj.core.api.WithAssertions; import org.assertj.core.api.WithAssertions;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.*; import java.awt.*;
@ -12,10 +11,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -24,57 +22,44 @@ public class BoxFitterTest
implements WithAssertions { implements WithAssertions {
private final BoxFitter boxFitter = TextFit.fitter(); private final BoxFitter boxFitter = TextFit.fitter();
private final int imageSize = 300;
private final int fontSize = 20;
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
private final Font font; private final Font font;
private Function<Integer, Font> fontFactory; private Function<Integer, Font> fontFactory;
private final int imageSize = 300;
private final Graphics2D graphics2D = graphics(imageSize, imageSize);
private Rectangle2D box = new Rectangle(imageSize, imageSize); private Rectangle2D box = new Rectangle(imageSize, imageSize);
public BoxFitterTest() throws URISyntaxException, IOException, FontFormatException { public BoxFitterTest() throws URISyntaxException, IOException, FontFormatException {
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf"); URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
int initialFontSize = 20;
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI())) font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
.deriveFont(Font.PLAIN, initialFontSize); .deriveFont(Font.PLAIN, fontSize);
fontFactory = size -> font.deriveFont(Font.PLAIN, size); fontFactory = size -> font.deriveFont(Font.PLAIN, size);
} }
interface FitTests {
int fit(String text);
}
@Nested
@DisplayName("Single Box API")
public class SingleBoxAPI implements FitTests {
@Override
public int fit(String longText) {
return boxFitter.fit(longText, fontFactory, graphics2D, box);
}
@Test @Test
@DisplayName("Fit single words") @DisplayName("Fit single words")
public void fitSingleWord() { public void fitSingleWord() {
Map<String, Integer> wordMap = Map.of( Map<String, Integer> wordMap = Map.of(
".", 263, ".", 263,
"a", 263, "a", 263,
"Word", 110, "Word", 121,
"longer", 96, "longer", 104,
"extralongword", 43 "extralongword", 44
); );
wordMap.forEach((word, expectedSize) -> wordMap.forEach((word, expectedSize) ->
assertThat(fit(word)) assertThat(invoke(word))
.as(word) .as(word)
.isEqualTo(expectedSize)); .isEqualTo(expectedSize));
} }
@Test @Test
@DisplayName("Fit various lengths") @DisplayName("Fit two words")
public void fitVariousLengths() { public void fitTwoWords() {
Map<String, Integer> wordMap = Map.of( Map<String, Integer> wordMap = Map.of(
". .", 263, ". .", 263,
"a a", 208, "a a", 208,
"Another Word", 76, "Another Word", 81,
longStringGenerator(1), 93, longStringGenerator(1), 100,
longStringGenerator(2), 36, longStringGenerator(2), 36,
longStringGenerator(3), 27, longStringGenerator(3), 27,
longStringGenerator(4), 22, longStringGenerator(4), 22,
@ -83,96 +68,30 @@ public class BoxFitterTest
longStringGenerator(196), 3 longStringGenerator(196), 3
); );
wordMap.forEach((word, expectedSize) -> wordMap.forEach((word, expectedSize) ->
assertThat(fit(word)) assertThat(invoke(word))
.as(word) .as(word)
.isEqualTo(expectedSize)); .isEqualTo(expectedSize));
} }
@Test @Test
@DisplayName("Text too long to fit throws") @DisplayName("Text too long to fit throws and exception")
// too long to fit means it would need to be rendered at a font size of <2 // too long to fit means it would need to be rendered at a font size of <2
public void tooLongThrows() { public void tooLongThrows() {
String longText = longStringGenerator(197); String longText = longStringGenerator(197);
assertThatExceptionOfType(NotEnoughSpace.class) assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> fit(longText)); .isThrownBy(() -> invoke(longText));
} }
@Test @Test
@DisplayName("Long text can be fitted down to a font size of 3") @DisplayName("Long text can be fitted down to a font size of 3")
public void veryLongFits() { public void veryLongFits() {
String longText = longStringGenerator(196); String longText = longStringGenerator(196);
assertThatCode(() -> fit(longText)) assertThatCode(() -> invoke(longText))
.doesNotThrowAnyException();
}
}
@Nested
@DisplayName("List of Boxes API")
public class BoxListAPI {
@Nested
@DisplayName("Single Box")
// different API, but should have same behaviour as using single box API
public class SingleBox implements FitTests {
private final List<Rectangle2D> boxes = Collections.singletonList(box);
@Override
public int fit(String longText) {
return boxFitter.fit(longText, fontFactory, graphics2D, boxes);
}
@Test
@DisplayName("Fit various lengths")
public void fitVariousLengths() {
Map<String, Integer> wordMap = Map.of(
". .", 263,
"a a", 208,
"Another Word", 76,
longStringGenerator(1), 93,
longStringGenerator(2), 36,
longStringGenerator(3), 27,
longStringGenerator(4), 22,
longStringGenerator(5), 20,
longStringGenerator(100), 4,
longStringGenerator(196), 3
);
wordMap.forEach((word, expectedSize) ->
assertThat(fit(word))
.as(word)
.isEqualTo(expectedSize));
}
@Test
@DisplayName("Excess text for one box - throws")
public void tooMuchForOneBox() {
assertThatExceptionOfType(NotEnoughSpace.class)
.isThrownBy(() ->
fit(longStringGenerator(197)));
}
}
@Nested
@DisplayName("Two Boxes")
public class TwoBoxes implements FitTests{
private final List<Rectangle2D> boxes = Arrays.asList(box, box);
@Override
public int fit(String longText) {
return boxFitter.fit(longText, fontFactory, graphics2D, boxes);
}
@Test
@DisplayName("Excess text for one box - fits into two")
public void tooLongThrows() {
assertThatCode(() ->
fit(longStringGenerator(197)))
.doesNotThrowAnyException(); .doesNotThrowAnyException();
} }
} private int invoke(String longText) {
return boxFitter.fit(longText, fontFactory, graphics2D, box);
} }
private String longStringGenerator(int cycles) { private String longStringGenerator(int cycles) {

View file

@ -1,40 +0,0 @@
package net.kemitix.text.fit;
import org.assertj.core.api.WithAssertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class StreamZipperTest implements WithAssertions {
@Test
void privateUtilityConstructor() throws NoSuchMethodException {
//given
final Constructor<StreamZipper> constructor = StreamZipper.class.getDeclaredConstructor();
constructor.setAccessible(true);
//then
assertThatCode(constructor::newInstance)
.hasCauseInstanceOf(UnsupportedOperationException.class);
}
@Test
@DisplayName("Pair two lists together")
void pairItems() {
//when
final List<String> strings = Arrays.asList("One", "Two", "Three");
final List<Integer> integers = Arrays.asList(3, 2, 1);
final List<Tuple<String, Integer>> zipped =
StreamZipper.zip(strings, integers, Tuple::of).collect(Collectors.toList());
//then
assertThat(zipped)
.extracting(Tuple::get1)
.containsExactlyElementsOf(strings.subList(0, zipped.size()));
assertThat(zipped)
.extracting(Tuple::get2)
.containsExactlyElementsOf(integers.subList(0, zipped.size()));
}
}

View file

@ -1,41 +1,25 @@
package net.kemitix.text.fit; package net.kemitix.text.fit;
import org.assertj.core.api.WithAssertions; import org.assertj.core.api.WithAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Nested;
import java.awt.*; import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class TextLineWrapTest public class TextLineWrapTest
implements WithAssertions { implements WithAssertions {
private static final String WORD = "word";
private static final String SPACE = " ";
private final WordWrapper textLineWrap = TextFit.wrapper(); private final WordWrapper textLineWrap = TextFit.wrapper();
private final int imageSize = 300; private final int imageSize = 300;
private final int fontSize = 20; private final int fontSize = 20;
private final Graphics2D graphics2D = graphics(imageSize, imageSize); private final Graphics2D graphics2D = graphics(imageSize, imageSize);
private final Font font; private final Font font;
private final int spaceWidth;
private final int wordWidth;
private final int wordsPerLine;
private final int wordHeight;
private final int linesPerBox;
public TextLineWrapTest() public TextLineWrapTest()
throws FontFormatException, throws FontFormatException,
@ -44,27 +28,8 @@ public class TextLineWrapTest
URL resource = this.getClass().getResource("alice/Alice-Regular.ttf"); URL resource = this.getClass().getResource("alice/Alice-Regular.ttf");
font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI())) font = Font.createFont(Font.TRUETYPE_FONT, new File(resource.toURI()))
.deriveFont(Font.PLAIN, fontSize); .deriveFont(Font.PLAIN, fontSize);
Rectangle2D spaceBounds = stringBounds(SPACE, font, graphics2D);
Rectangle2D wordBounds = stringBounds(WORD, font, graphics2D);
spaceWidth = (int) spaceBounds.getWidth();
wordWidth = (int) wordBounds.getWidth();
wordHeight = (int) wordBounds.getHeight();
wordsPerLine = imageSize / (wordWidth + spaceWidth);
linesPerBox = imageSize / wordHeight;
} }
private Rectangle2D stringBounds(
String string,
Font font,
Graphics2D graphics2D
) {
return font.getStringBounds(string, graphics2D.getFontRenderContext());
}
@Nested
@DisplayName("Single box")
public class SingleBox {
private List<String> invoke(String in) { private List<String> invoke(String in) {
return textLineWrap.wrap(in, font, graphics2D, imageSize); return textLineWrap.wrap(in, font, graphics2D, imageSize);
} }
@ -82,49 +47,27 @@ public class TextLineWrapTest
} }
@Test @Test
@DisplayName("Fits max 'words' per line on one line") @DisplayName("Longer string fits on two lines")
public void fitMaxWordsOneLine() { public void longerStringOnTwoLines() {
String input = words(wordsPerLine, WORD); assertThat(invoke(
assertThat(invoke(input)).containsExactly(input); "xxxxxxxxxxx xxxxxxxxxxxx " +
} "xxxxxxxxxxx xxxxxxxxxxxx"))
@Test
@DisplayName("Wraps just over max 'words' per line onto two lines")
public void wrapOneWordTooManyOntoTwoLines() {
assertThat(invoke(words(wordsPerLine + 1, WORD)))
.containsExactly( .containsExactly(
words(wordsPerLine, WORD), "xxxxxxxxxxx xxxxxxxxxxxx",
WORD); "xxxxxxxxxxx xxxxxxxxxxxx");
} }
@Test @Test
@DisplayName("Wraps onto three lines") @DisplayName("Longer string fits on three lines")
public void longerStringOnThreeLines() { public void longerStringOnThreeLines() {
String oneLinesWorthOfWords = words(wordsPerLine, WORD); assertThat(invoke(
assertThat(invoke(words(wordsPerLine + wordsPerLine + 1, WORD))) "xxxxxxxxxxx xxxxxxxxxxxx " +
"xxxxxxxxxxx xxxxxxxxxxxx " +
"xxxxxxxxxxx xxxxxxxxxxxx"))
.containsExactly( .containsExactly(
oneLinesWorthOfWords, "xxxxxxxxxxx xxxxxxxxxxxx",
oneLinesWorthOfWords, "xxxxxxxxxxx xxxxxxxxxxxx",
WORD); "xxxxxxxxxxx xxxxxxxxxxxx");
}
@Test
@DisplayName("A word that can't fit on a line by itself throws")
public void overLongWordThrows() {
String longWord = words(wordsPerLine * 2, WORD)
.replace(" ", "");
assertThatExceptionOfType(WordTooLong.class)
.isThrownBy(() -> invoke(longWord))
.satisfies(error ->
assertThat(error.getLongWord())
.isEqualTo(longWord));
}
}
private String words(int number, String word) {
return IntStream.range(0, number)
.mapToObj(i -> word + SPACE)
.collect(Collectors.joining()).trim();
} }
private Graphics2D graphics(int width, int height) { private Graphics2D graphics(int width, int height) {
@ -135,181 +78,4 @@ public class TextLineWrapTest
private BufferedImage image(int width, int height) { private BufferedImage image(int width, int height) {
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
} }
@Nested
@DisplayName("Overflowing Boxes")
public class OverflowingBoxes {
private List<Rectangle2D> boxes = new ArrayList<>();
private List<List<String>> wrap(String in) {
return textLineWrap.wrap(in, font, graphics2D, boxes);
}
@Nested
@DisplayName("Single box")
public class SingleBox {
@BeforeEach
public void setUp() {
boxes.add(new Rectangle(imageSize, imageSize));
}
@Test
@DisplayName("Empty String give empty List")
public void emptyStringEmptyList() {
assertThat(wrap("")).containsExactly(Collections.emptyList());
}
@Test
@DisplayName("Short string fits on one line")
public void shortStringOnOneLine() {
assertThat(wrap("x"))
.containsExactly(Collections.singletonList("x"));
}
@Test
@DisplayName("Fits max 'words' per line on one line")
public void fitMaxWordsOneLine() {
String input = words(wordsPerLine, WORD);
assertThat(wrap(input))
.containsExactly(Collections.singletonList(input));
}
@Test
@DisplayName("Wraps just over max 'words' per line onto two lines")
public void wrapOneWordTooManyOntoTwoLines() {
String input = words(wordsPerLine + 1, WORD);
assertThat(wrap(input)).containsExactly(Arrays.asList(
words(wordsPerLine, WORD),
WORD));
}
@Test
@DisplayName("Wraps onto three lines")
public void longerStringOnThreeLines() {
String input = words(wordsPerLine + wordsPerLine + 1, WORD);
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
assertThat(wrap(input)).containsExactly(Arrays.asList(
oneLinesWorthOfWords,
oneLinesWorthOfWords,
WORD));
}
@Test
@DisplayName("Text fills the box")
public void textFillsBox() {
String lineOfWords = words(wordsPerLine, WORD);
String words = words(linesPerBox, lineOfWords);
assertThat(wrap(words).get(0))
.hasSize(linesPerBox)
.allSatisfy(line ->
assertThat(line).isEqualTo(lineOfWords));
}
@Test
@DisplayName("Text overflows the box")
public void tooManyLinesForBox() {
String lineOfWords = words(wordsPerLine, WORD);
String input = words(linesPerBox + 1, lineOfWords);
assertThatExceptionOfType(NotEnoughSpace.class)
.isThrownBy(() -> wrap(input))
.satisfies(error ->
assertThat(error.getExcessWordCount())
.isEqualTo(wordsPerLine));
}
}
@Nested
@DisplayName("Two boxes of equal width")
public class TwoEqualWidthBoxes {
@BeforeEach
public void setUp() {
Rectangle box = new Rectangle(imageSize, imageSize);
boxes.add(box);
boxes.add(box);
}
// Check that even with two boxes the shorted strings are unaffected
@Test
@DisplayName("Empty String give empty List")
public void emptyStringEmptyList() {
assertThat(wrap("")).containsExactly(
Collections.emptyList(),
Collections.emptyList());
}
@Test
@DisplayName("Short string fits on one line")
public void shortStringOnOneLine() {
assertThat(wrap("x"))
.containsExactly(
Collections.singletonList("x"),
Collections.emptyList());
}
@Test
@DisplayName("Fits max 'words' per line on one line")
public void fitMaxWordsOneLine() {
String input = words(wordsPerLine, WORD);
assertThat(wrap(input))
.containsExactly(
Collections.singletonList(input),
Collections.emptyList() );
}
@Test
@DisplayName("Wraps just over max 'words' per line onto two lines")
public void wrapOneWordTooManyOntoTwoLines() {
String input = words(wordsPerLine + 1, WORD);
assertThat(wrap(input)).containsExactly(Arrays.asList(
words(wordsPerLine, WORD),
WORD),
Collections.emptyList());
}
@Test
@DisplayName("Wraps onto three lines")
public void longerStringOnThreeLines() {
String input = words(wordsPerLine + wordsPerLine + 1, WORD);
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
assertThat(wrap(input)).containsExactly(Arrays.asList(
oneLinesWorthOfWords,
oneLinesWorthOfWords,
WORD),
Collections.emptyList());
}
@Test
@DisplayName("Fit max lines into the first box")
public void fitMaxLinesInFirstBox() {
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
String input = words(linesPerBox, oneLinesWorthOfWords);
List<List<String>> result = wrap(input);
List<String> linesBoxOne = result.get(0);
assertThat(linesBoxOne)
.hasSize(linesPerBox)
.allSatisfy(line ->
assertThat(line)
.isEqualTo(oneLinesWorthOfWords));
}
@Test
@DisplayName("Overflow just over max lines into the second box")
public void overflowMaxPlusOneLinesIntoSecondBox() {
String oneLinesWorthOfWords = words(wordsPerLine, WORD);
String input = words(linesPerBox + 1, oneLinesWorthOfWords);
List<List<String>> result = wrap(input);
List<String> linesBoxOne = result.get(0);
assertThat(linesBoxOne)
.hasSize(linesPerBox)
.allSatisfy(line ->
assertThat(line)
.isEqualTo(oneLinesWorthOfWords));
assertThat(result.get(1))
.containsExactly(oneLinesWorthOfWords);
}
}
}
} }